Skip to content

Commit

Permalink
Add field and rule value to violations (#215)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
jchadwick-buf authored Dec 12, 2024
1 parent fabdc93 commit df11bf2
Show file tree
Hide file tree
Showing 28 changed files with 862 additions and 505 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,6 @@
import build.buf.protovalidate.exceptions.CompilationException;
import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.ValidateProto;
import build.buf.validate.Violation;
import build.buf.validate.Violations;
import build.buf.validate.conformance.harness.TestConformanceRequest;
import build.buf.validate.conformance.harness.TestConformanceResponse;
Expand Down Expand Up @@ -100,11 +99,11 @@ static TestResult testCase(
private static TestResult validate(Validator validator, DynamicMessage dynamicMessage) {
try {
ValidationResult result = validator.validate(dynamicMessage);
List<Violation> violations = result.getViolations();
if (violations.isEmpty()) {
if (result.isSuccess()) {
return TestResult.newBuilder().setSuccess(true).build();
}
Violations error = Violations.newBuilder().addAllViolations(violations).build();
Violations error =
Violations.newBuilder().addAllViolations(result.toProto().getViolationsList()).build();
return TestResult.newBuilder().setValidationError(error).build();
} catch (CompilationException e) {
return TestResult.newBuilder().setCompilationError(e.getMessage()).build();
Expand Down
24 changes: 20 additions & 4 deletions src/main/java/build/buf/protovalidate/ValidationResult.java
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,8 @@

package build.buf.protovalidate;

import build.buf.validate.Violation;
import build.buf.validate.Violations;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

Expand Down Expand Up @@ -71,12 +72,27 @@ public String toString() {
builder.append("Validation error:");
for (Violation violation : violations) {
builder.append("\n - ");
if (!violation.getFieldPath().isEmpty()) {
builder.append(violation.getFieldPath());
if (!violation.toProto().getFieldPath().isEmpty()) {
builder.append(violation.toProto().getFieldPath());
builder.append(": ");
}
builder.append(String.format("%s [%s]", violation.getMessage(), violation.getConstraintId()));
builder.append(
String.format(
"%s [%s]", violation.toProto().getMessage(), violation.toProto().getConstraintId()));
}
return builder.toString();
}

/**
* Converts the validation result to its equivalent protobuf form.
*
* @return The protobuf form of this validation result.
*/
public build.buf.validate.Violations toProto() {
List<build.buf.validate.Violation> protoViolations = new ArrayList<>();
for (Violation violation : violations) {
protoViolations.add(violation.toProto());
}
return Violations.newBuilder().addAllViolations(protoViolations).build();
}
}
16 changes: 11 additions & 5 deletions src/main/java/build/buf/protovalidate/Validator.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,14 @@
import build.buf.protovalidate.exceptions.CompilationException;
import build.buf.protovalidate.exceptions.ValidationException;
import build.buf.protovalidate.internal.celext.ValidateLibrary;
import build.buf.protovalidate.internal.errors.FieldPathUtils;
import build.buf.protovalidate.internal.errors.ConstraintViolation;
import build.buf.protovalidate.internal.evaluator.Evaluator;
import build.buf.protovalidate.internal.evaluator.EvaluatorBuilder;
import build.buf.protovalidate.internal.evaluator.MessageValue;
import com.google.protobuf.Descriptors.Descriptor;
import com.google.protobuf.Message;
import java.util.ArrayList;
import java.util.List;
import org.projectnessie.cel.Env;
import org.projectnessie.cel.Library;

Expand Down Expand Up @@ -75,11 +77,15 @@ public ValidationResult validate(Message msg) throws ValidationException {
}
Descriptor descriptor = msg.getDescriptorForType();
Evaluator evaluator = evaluatorBuilder.load(descriptor);
ValidationResult result = evaluator.evaluate(new MessageValue(msg), failFast);
if (result.isSuccess()) {
return result;
List<ConstraintViolation.Builder> result = evaluator.evaluate(new MessageValue(msg), failFast);
if (result.isEmpty()) {
return ValidationResult.EMPTY;
}
List<Violation> violations = new ArrayList<>(result.size());
for (ConstraintViolation.Builder builder : result) {
violations.add(builder.build());
}
return new ValidationResult(FieldPathUtils.calculateFieldPathStrings(result.getViolations()));
return new ValidationResult(violations);
}

/**
Expand Down
65 changes: 65 additions & 0 deletions src/main/java/build/buf/protovalidate/Violation.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright 2023-2024 Buf Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package build.buf.protovalidate;

import com.google.protobuf.Descriptors;
import javax.annotation.Nullable;

/**
* {@link Violation} provides all of the collected information about an individual constraint
* violation.
*/
public interface Violation {
/** {@link FieldValue} represents a Protobuf field value inside a Protobuf message. */
interface FieldValue {
/**
* Gets the value of the field, which may be null, a primitive, a Map or a List.
*
* @return The value of the protobuf field.
*/
@Nullable
Object getValue();

/**
* Gets the field descriptor of the field this value is from.
*
* @return A FieldDescriptor pertaining to this field.
*/
Descriptors.FieldDescriptor getDescriptor();
}

/**
* Gets the protobuf form of this violation.
*
* @return The protobuf form of this violation.
*/
build.buf.validate.Violation toProto();

/**
* Gets the value of the field this violation pertains to, or null if there is none.
*
* @return Value of the field associated with the violation, or null if there is none.
*/
@Nullable
FieldValue getFieldValue();

/**
* Gets the value of the rule this violation pertains to, or null if there is none.
*
* @return Value of the rule associated with the violation, or null if there is none.
*/
@Nullable
FieldValue getRuleValue();
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@
import build.buf.protovalidate.Config;
import build.buf.protovalidate.exceptions.CompilationException;
import build.buf.protovalidate.internal.errors.FieldPathUtils;
import build.buf.protovalidate.internal.evaluator.ObjectValue;
import build.buf.protovalidate.internal.evaluator.Value;
import build.buf.protovalidate.internal.expression.AstExpression;
import build.buf.protovalidate.internal.expression.CompiledProgram;
import build.buf.protovalidate.internal.expression.Expression;
Expand Down Expand Up @@ -142,6 +144,7 @@ public List<CompiledProgram> compile(
Env ruleEnv = getRuleEnv(fieldDescriptor, message, rule.field, forItems);
Variable ruleVar = Variable.newRuleVariable(message, message.getField(rule.field));
ProgramOption globals = ProgramOption.globals(ruleVar);
Value ruleValue = new ObjectValue(rule.field, message.getField(rule.field));
try {
Program program = ruleEnv.program(rule.astExpression.ast, globals, PARTIAL_EVAL_OPTIONS);
Program.EvalResult evalResult = program.eval(Activation.emptyActivation());
Expand All @@ -158,13 +161,17 @@ public List<CompiledProgram> compile(
Ast residual = ruleEnv.residualAst(rule.astExpression.ast, evalResult.getEvalDetails());
programs.add(
new CompiledProgram(
ruleEnv.program(residual, globals), rule.astExpression.source, rule.rulePath));
ruleEnv.program(residual, globals),
rule.astExpression.source,
rule.rulePath,
ruleValue));
} catch (Exception e) {
programs.add(
new CompiledProgram(
ruleEnv.program(rule.astExpression.ast, globals),
rule.astExpression.source,
rule.rulePath));
rule.rulePath,
ruleValue));
}
}
return Collections.unmodifiableList(programs);
Expand Down
Loading

0 comments on commit df11bf2

Please sign in to comment.