Skip to content

Commit 43bf841

Browse files
committed
NIFI-15587 - Add extensible FieldValidator and RecordValidator support to the Record API
Signed-off-by: Pierre Villard <pierre.villard.fr@gmail.com>
1 parent bbb2b15 commit 43bf841

8 files changed

Lines changed: 881 additions & 29 deletions

File tree

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.serialization.record.validation;
18+
19+
import java.util.Objects;
20+
import java.util.Optional;
21+
import java.util.StringJoiner;
22+
23+
/**
24+
* Basic implementation of {@link ValidationError} that can be used by validators in modules that
25+
* cannot depend on higher level utility classes. Instances are immutable and thread-safe.
26+
*/
27+
public class DefaultValidationError implements ValidationError {
28+
private final Optional<String> fieldName;
29+
private final Optional<Object> inputValue;
30+
private final String explanation;
31+
private final ValidationErrorType type;
32+
33+
private DefaultValidationError(final Builder builder) {
34+
this.fieldName = Optional.ofNullable(builder.fieldName);
35+
this.inputValue = Optional.ofNullable(builder.inputValue);
36+
this.explanation = Objects.requireNonNull(builder.explanation, "Explanation is required");
37+
this.type = Objects.requireNonNull(builder.type, "Validation error type is required");
38+
}
39+
40+
@Override
41+
public Optional<String> getFieldName() {
42+
return fieldName;
43+
}
44+
45+
@Override
46+
public Optional<Object> getInputValue() {
47+
return inputValue;
48+
}
49+
50+
@Override
51+
public String getExplanation() {
52+
return explanation;
53+
}
54+
55+
@Override
56+
public ValidationErrorType getType() {
57+
return type;
58+
}
59+
60+
@Override
61+
public String toString() {
62+
final StringJoiner joiner = new StringJoiner(", ", "DefaultValidationError[", "]");
63+
fieldName.ifPresent(name -> joiner.add("field=" + name));
64+
inputValue.ifPresent(value -> joiner.add("value=" + value));
65+
joiner.add("type=" + type);
66+
joiner.add("explanation=" + explanation);
67+
return joiner.toString();
68+
}
69+
70+
@Override
71+
public int hashCode() {
72+
return 31 + 17 * fieldName.hashCode() + 17 * inputValue.hashCode() + 17 * explanation.hashCode();
73+
}
74+
75+
@Override
76+
public boolean equals(final Object obj) {
77+
if (obj == this) {
78+
return true;
79+
}
80+
if (obj == null) {
81+
return false;
82+
}
83+
if (!(obj instanceof ValidationError other)) {
84+
return false;
85+
}
86+
return getFieldName().equals(other.getFieldName()) && getInputValue().equals(other.getInputValue()) && getExplanation().equals(other.getExplanation());
87+
}
88+
89+
/**
90+
* Creates a builder for constructing immutable {@link DefaultValidationError} instances.
91+
*
92+
* @return builder instance
93+
*/
94+
public static Builder builder() {
95+
return new Builder();
96+
}
97+
98+
public static final class Builder {
99+
private String fieldName;
100+
private Object inputValue;
101+
private String explanation;
102+
private ValidationErrorType type = ValidationErrorType.INVALID_FIELD;
103+
104+
private Builder() {
105+
}
106+
107+
public Builder fieldName(final String fieldName) {
108+
this.fieldName = fieldName;
109+
return this;
110+
}
111+
112+
public Builder inputValue(final Object inputValue) {
113+
this.inputValue = inputValue;
114+
return this;
115+
}
116+
117+
public Builder explanation(final String explanation) {
118+
this.explanation = explanation;
119+
return this;
120+
}
121+
122+
public Builder type(final ValidationErrorType type) {
123+
this.type = type;
124+
return this;
125+
}
126+
127+
public DefaultValidationError build() {
128+
return new DefaultValidationError(this);
129+
}
130+
}
131+
}
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.serialization.record.validation;
18+
19+
import java.util.Collection;
20+
21+
/**
22+
* Provides validation for an individual record field value. Field Validators are expected to be immutable and thread-safe.
23+
* If a validator needs field metadata (such as the data type), it should capture that information at construction time.
24+
*/
25+
public interface FieldValidator {
26+
27+
/**
28+
* Validates the provided value for a field at the given path.
29+
*
30+
* @param fieldPath the path of the field being validated (used for clear diagnostics)
31+
* @param value the value of the field for the record currently being validated
32+
* @return a collection of validation errors. The collection must be empty when the value is valid.
33+
*/
34+
Collection<ValidationError> validate(String fieldPath, Object value);
35+
36+
/**
37+
* @return a short human readable description of what the validator enforces
38+
*/
39+
String getDescription();
40+
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.serialization.record.validation;
18+
19+
import org.apache.nifi.serialization.record.Record;
20+
21+
import java.util.Collection;
22+
23+
/**
24+
* Provides validation logic for an entire {@link Record} instance.
25+
* Record Validators are expected to be immutable and thread-safe.
26+
* If a validator needs schema metadata, it should capture that information at construction time.
27+
*/
28+
public interface RecordValidator {
29+
30+
/**
31+
* Validates the provided record.
32+
*
33+
* @param record the record instance to validate
34+
* @param fieldPath the path within the overall document that identifies the record (root records use the empty string)
35+
* @return a collection of validation errors. The collection must be empty when the record is valid.
36+
*/
37+
Collection<ValidationError> validate(Record record, String fieldPath);
38+
39+
/**
40+
* @return a short human readable description of what the validator enforces
41+
*/
42+
String getDescription();
43+
}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
/*
2+
* Licensed to the Apache Software Foundation (ASF) under one or more
3+
* contributor license agreements. See the NOTICE file distributed with
4+
* this work for additional information regarding copyright ownership.
5+
* The ASF licenses this file to You under the Apache License, Version 2.0
6+
* (the "License"); you may not use this file except in compliance with
7+
* the License. You may obtain a copy of the License at
8+
*
9+
* http://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
package org.apache.nifi.serialization.record.validation;
18+
19+
import java.util.Collections;
20+
import java.util.HashMap;
21+
import java.util.List;
22+
import java.util.Map;
23+
24+
/**
25+
* Immutable container that carries field-level and record-level validators independently of the schema model.
26+
* Field validators are keyed by field name. Record validators apply to the entire record.
27+
* Nested validators mirror the schema tree: a RECORD-type or ARRAY-of-RECORD field maps to a child
28+
* {@code SchemaValidators} that applies to the nested record.
29+
*/
30+
public class SchemaValidators {
31+
public static final SchemaValidators EMPTY = new SchemaValidators(Map.of(), List.of(), Map.of());
32+
33+
private final Map<String, List<FieldValidator>> fieldValidators;
34+
private final List<RecordValidator> recordValidators;
35+
private final Map<String, SchemaValidators> nestedValidators;
36+
37+
public SchemaValidators(final Map<String, List<FieldValidator>> fieldValidators, final List<RecordValidator> recordValidators) {
38+
this(fieldValidators, recordValidators, Map.of());
39+
}
40+
41+
public SchemaValidators(final Map<String, List<FieldValidator>> fieldValidators, final List<RecordValidator> recordValidators,
42+
final Map<String, SchemaValidators> nestedValidators) {
43+
if (fieldValidators == null || fieldValidators.isEmpty()) {
44+
this.fieldValidators = Map.of();
45+
} else {
46+
final Map<String, List<FieldValidator>> defensiveCopy = new HashMap<>(fieldValidators.size());
47+
for (final Map.Entry<String, List<FieldValidator>> entry : fieldValidators.entrySet()) {
48+
defensiveCopy.put(entry.getKey(), List.copyOf(entry.getValue()));
49+
}
50+
this.fieldValidators = Collections.unmodifiableMap(defensiveCopy);
51+
}
52+
this.recordValidators = recordValidators == null || recordValidators.isEmpty() ? List.of() : List.copyOf(recordValidators);
53+
this.nestedValidators = nestedValidators == null || nestedValidators.isEmpty() ? Map.of() : Map.copyOf(nestedValidators);
54+
}
55+
56+
public List<FieldValidator> getFieldValidators(final String fieldName) {
57+
return fieldValidators.getOrDefault(fieldName, List.of());
58+
}
59+
60+
public Map<String, List<FieldValidator>> getAllFieldValidators() {
61+
return fieldValidators;
62+
}
63+
64+
public List<RecordValidator> getRecordValidators() {
65+
return recordValidators;
66+
}
67+
68+
public SchemaValidators getNestedValidators(final String fieldName) {
69+
return nestedValidators.getOrDefault(fieldName, EMPTY);
70+
}
71+
72+
public boolean isEmpty() {
73+
return fieldValidators.isEmpty() && recordValidators.isEmpty() && nestedValidators.isEmpty();
74+
}
75+
}

0 commit comments

Comments
 (0)