Skip to content

Commit df11bf2

Browse files
author
jchadwick-buf
authored
Add field and rule value to violations (#215)
Adds the ability to access the captured rule and field value from a Violation. There's a bit of refactoring toil, so I tried to split the commits cleanly to make this easier to review. - The first commit adds the `Violation` wrapper class and plumbs it through all uses of `Violation`. - The second commit makes `Value` non-internal so we can use it to expose protobuf values in a cleaner fashion. - The third commit actually implements filling the `fieldValue` and `ruleValue`fields. **This is a breaking change.** The API changes in the following ways: - `ValidationResult` now provides a new wrapper `Violation` type instead of the `buf.validate.Violation` message. This new wrapper has a `getProto()` method to return a `buf.validate.Violation` message, and `ValidationResult` now has a `toProto()` method to return a `buf.validate.Violations` message.
1 parent fabdc93 commit df11bf2

28 files changed

+862
-505
lines changed

conformance/src/main/java/build/buf/protovalidate/conformance/Main.java

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@
2020
import build.buf.protovalidate.exceptions.CompilationException;
2121
import build.buf.protovalidate.exceptions.ExecutionException;
2222
import build.buf.validate.ValidateProto;
23-
import build.buf.validate.Violation;
2423
import build.buf.validate.Violations;
2524
import build.buf.validate.conformance.harness.TestConformanceRequest;
2625
import build.buf.validate.conformance.harness.TestConformanceResponse;
@@ -100,11 +99,11 @@ static TestResult testCase(
10099
private static TestResult validate(Validator validator, DynamicMessage dynamicMessage) {
101100
try {
102101
ValidationResult result = validator.validate(dynamicMessage);
103-
List<Violation> violations = result.getViolations();
104-
if (violations.isEmpty()) {
102+
if (result.isSuccess()) {
105103
return TestResult.newBuilder().setSuccess(true).build();
106104
}
107-
Violations error = Violations.newBuilder().addAllViolations(violations).build();
105+
Violations error =
106+
Violations.newBuilder().addAllViolations(result.toProto().getViolationsList()).build();
108107
return TestResult.newBuilder().setValidationError(error).build();
109108
} catch (CompilationException e) {
110109
return TestResult.newBuilder().setCompilationError(e.getMessage()).build();

src/main/java/build/buf/protovalidate/ValidationResult.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,8 @@
1414

1515
package build.buf.protovalidate;
1616

17-
import build.buf.validate.Violation;
17+
import build.buf.validate.Violations;
18+
import java.util.ArrayList;
1819
import java.util.Collections;
1920
import java.util.List;
2021

@@ -71,12 +72,27 @@ public String toString() {
7172
builder.append("Validation error:");
7273
for (Violation violation : violations) {
7374
builder.append("\n - ");
74-
if (!violation.getFieldPath().isEmpty()) {
75-
builder.append(violation.getFieldPath());
75+
if (!violation.toProto().getFieldPath().isEmpty()) {
76+
builder.append(violation.toProto().getFieldPath());
7677
builder.append(": ");
7778
}
78-
builder.append(String.format("%s [%s]", violation.getMessage(), violation.getConstraintId()));
79+
builder.append(
80+
String.format(
81+
"%s [%s]", violation.toProto().getMessage(), violation.toProto().getConstraintId()));
7982
}
8083
return builder.toString();
8184
}
85+
86+
/**
87+
* Converts the validation result to its equivalent protobuf form.
88+
*
89+
* @return The protobuf form of this validation result.
90+
*/
91+
public build.buf.validate.Violations toProto() {
92+
List<build.buf.validate.Violation> protoViolations = new ArrayList<>();
93+
for (Violation violation : violations) {
94+
protoViolations.add(violation.toProto());
95+
}
96+
return Violations.newBuilder().addAllViolations(protoViolations).build();
97+
}
8298
}

src/main/java/build/buf/protovalidate/Validator.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,12 +17,14 @@
1717
import build.buf.protovalidate.exceptions.CompilationException;
1818
import build.buf.protovalidate.exceptions.ValidationException;
1919
import build.buf.protovalidate.internal.celext.ValidateLibrary;
20-
import build.buf.protovalidate.internal.errors.FieldPathUtils;
20+
import build.buf.protovalidate.internal.errors.ConstraintViolation;
2121
import build.buf.protovalidate.internal.evaluator.Evaluator;
2222
import build.buf.protovalidate.internal.evaluator.EvaluatorBuilder;
2323
import build.buf.protovalidate.internal.evaluator.MessageValue;
2424
import com.google.protobuf.Descriptors.Descriptor;
2525
import com.google.protobuf.Message;
26+
import java.util.ArrayList;
27+
import java.util.List;
2628
import org.projectnessie.cel.Env;
2729
import org.projectnessie.cel.Library;
2830

@@ -75,11 +77,15 @@ public ValidationResult validate(Message msg) throws ValidationException {
7577
}
7678
Descriptor descriptor = msg.getDescriptorForType();
7779
Evaluator evaluator = evaluatorBuilder.load(descriptor);
78-
ValidationResult result = evaluator.evaluate(new MessageValue(msg), failFast);
79-
if (result.isSuccess()) {
80-
return result;
80+
List<ConstraintViolation.Builder> result = evaluator.evaluate(new MessageValue(msg), failFast);
81+
if (result.isEmpty()) {
82+
return ValidationResult.EMPTY;
83+
}
84+
List<Violation> violations = new ArrayList<>(result.size());
85+
for (ConstraintViolation.Builder builder : result) {
86+
violations.add(builder.build());
8187
}
82-
return new ValidationResult(FieldPathUtils.calculateFieldPathStrings(result.getViolations()));
88+
return new ValidationResult(violations);
8389
}
8490

8591
/**
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
// Copyright 2023-2024 Buf Technologies, Inc.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package build.buf.protovalidate;
16+
17+
import com.google.protobuf.Descriptors;
18+
import javax.annotation.Nullable;
19+
20+
/**
21+
* {@link Violation} provides all of the collected information about an individual constraint
22+
* violation.
23+
*/
24+
public interface Violation {
25+
/** {@link FieldValue} represents a Protobuf field value inside a Protobuf message. */
26+
interface FieldValue {
27+
/**
28+
* Gets the value of the field, which may be null, a primitive, a Map or a List.
29+
*
30+
* @return The value of the protobuf field.
31+
*/
32+
@Nullable
33+
Object getValue();
34+
35+
/**
36+
* Gets the field descriptor of the field this value is from.
37+
*
38+
* @return A FieldDescriptor pertaining to this field.
39+
*/
40+
Descriptors.FieldDescriptor getDescriptor();
41+
}
42+
43+
/**
44+
* Gets the protobuf form of this violation.
45+
*
46+
* @return The protobuf form of this violation.
47+
*/
48+
build.buf.validate.Violation toProto();
49+
50+
/**
51+
* Gets the value of the field this violation pertains to, or null if there is none.
52+
*
53+
* @return Value of the field associated with the violation, or null if there is none.
54+
*/
55+
@Nullable
56+
FieldValue getFieldValue();
57+
58+
/**
59+
* Gets the value of the rule this violation pertains to, or null if there is none.
60+
*
61+
* @return Value of the rule associated with the violation, or null if there is none.
62+
*/
63+
@Nullable
64+
FieldValue getRuleValue();
65+
}

src/main/java/build/buf/protovalidate/internal/constraints/ConstraintCache.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@
1717
import build.buf.protovalidate.Config;
1818
import build.buf.protovalidate.exceptions.CompilationException;
1919
import build.buf.protovalidate.internal.errors.FieldPathUtils;
20+
import build.buf.protovalidate.internal.evaluator.ObjectValue;
21+
import build.buf.protovalidate.internal.evaluator.Value;
2022
import build.buf.protovalidate.internal.expression.AstExpression;
2123
import build.buf.protovalidate.internal.expression.CompiledProgram;
2224
import build.buf.protovalidate.internal.expression.Expression;
@@ -142,6 +144,7 @@ public List<CompiledProgram> compile(
142144
Env ruleEnv = getRuleEnv(fieldDescriptor, message, rule.field, forItems);
143145
Variable ruleVar = Variable.newRuleVariable(message, message.getField(rule.field));
144146
ProgramOption globals = ProgramOption.globals(ruleVar);
147+
Value ruleValue = new ObjectValue(rule.field, message.getField(rule.field));
145148
try {
146149
Program program = ruleEnv.program(rule.astExpression.ast, globals, PARTIAL_EVAL_OPTIONS);
147150
Program.EvalResult evalResult = program.eval(Activation.emptyActivation());
@@ -158,13 +161,17 @@ public List<CompiledProgram> compile(
158161
Ast residual = ruleEnv.residualAst(rule.astExpression.ast, evalResult.getEvalDetails());
159162
programs.add(
160163
new CompiledProgram(
161-
ruleEnv.program(residual, globals), rule.astExpression.source, rule.rulePath));
164+
ruleEnv.program(residual, globals),
165+
rule.astExpression.source,
166+
rule.rulePath,
167+
ruleValue));
162168
} catch (Exception e) {
163169
programs.add(
164170
new CompiledProgram(
165171
ruleEnv.program(rule.astExpression.ast, globals),
166172
rule.astExpression.source,
167-
rule.rulePath));
173+
rule.rulePath,
174+
ruleValue));
168175
}
169176
}
170177
return Collections.unmodifiableList(programs);

0 commit comments

Comments
 (0)