Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -339,12 +339,12 @@ dependencies {
annotationProcessor(libs.nullaway)
api(libs.jspecify)
api(libs.protobuf.java)
implementation(enforcedPlatform(libs.cel))
implementation(libs.cel.core)
implementation(libs.cel)

buf("build.buf:buf:${libs.versions.buf.get()}:${osdetector.classifier}@exe")

testImplementation(libs.assertj)
testImplementation(libs.grpc.protobuf)
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For google.rpc.Status

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where is that used?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In some of the cel protos we generate code for

testImplementation(platform(libs.junit.bom))
testImplementation("org.junit.jupiter:junit-jupiter")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
Expand Down
9 changes: 9 additions & 0 deletions conformance/expected-failures.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
standard_rules/bytes:
- pattern/invalid/not_utf8
# input: [ type.googleapis.com/buf.validate.conformance.cases.BytesPattern ]:{val:"\x99"}
# want: runtime error: value must be valid UTF-8 to apply regexp
# got: validation error (1 violation)
# 1. rule_id: "bytes.pattern"
# message: "value must match regex pattern `^[\\x00-\\x7F]+$`"
# field: "val" elements:{field_number:1 field_name:"val" field_type:TYPE_BYTES}
# rule: "bytes.pattern" elements:{field_number:15 field_name:"bytes" field_type:TYPE_MESSAGE} elements:{field_number:4 field_name:"pattern" field_type:TYPE_STRING}
Comment on lines +1 to +9
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can be solved if we override the bytes_to_string: #304

6 changes: 3 additions & 3 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
[versions]
assertj = "3.27.3"
buf = "1.54.0"
cel = "0.5.3"
cel = "0.9.1"
error-prone = "2.38.0"
junit = "5.13.0"
maven-publish = "0.32.0"
Expand All @@ -10,10 +10,10 @@ protobuf = "4.31.1"
[libraries]
assertj = { module = "org.assertj:assertj-core", version.ref = "assertj" }
buf = { module = "build.buf:buf", version.ref = "buf" }
cel = { module = "org.projectnessie.cel:cel-bom", version.ref = "cel" }
cel-core = { module = "org.projectnessie.cel:cel-core" }
cel = { module = "dev.cel:cel", version.ref = "cel" }
errorprone-annotations = { module = "com.google.errorprone:error_prone_annotations", version.ref = "error-prone" }
errorprone-core = { module = "com.google.errorprone:error_prone_core", version.ref = "error-prone" }
grpc-protobuf = { module = "io.grpc:grpc-protobuf", version = "1.73.0" }
jspecify = { module ="org.jspecify:jspecify", version = "1.0.0" }
junit-bom = { module = "org.junit:junit-bom", version.ref = "junit" }
maven-plugin = { module = "com.vanniktech:gradle-maven-publish-plugin", version.ref = "maven-publish" }
Expand Down
41 changes: 25 additions & 16 deletions src/main/java/build/buf/protovalidate/AstExpression.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,46 +15,55 @@
package build.buf.protovalidate;

import build.buf.protovalidate.exceptions.CompilationException;
import com.google.api.expr.v1alpha1.Type;
import org.projectnessie.cel.Ast;
import org.projectnessie.cel.Env;
import dev.cel.common.CelAbstractSyntaxTree;
import dev.cel.common.CelValidationException;
import dev.cel.common.CelValidationResult;
import dev.cel.common.types.CelKind;
import dev.cel.compiler.CelCompiler;

/** {@link AstExpression} is a compiled CEL {@link Ast}. */
/** {@link AstExpression} is a compiled CEL {@link CelAbstractSyntaxTree}. */
class AstExpression {
/** The compiled CEL AST. */
public final Ast ast;
public final CelAbstractSyntaxTree ast;

/** Contains the original expression from the proto file. */
public final Expression source;

/** Constructs a new {@link AstExpression}. */
private AstExpression(Ast ast, Expression source) {
private AstExpression(CelAbstractSyntaxTree ast, Expression source) {
this.ast = ast;
this.source = source;
}

/**
* Compiles the given expression to a {@link AstExpression}.
*
* @param env The CEL environment.
* @param cel The CEL compiler.
* @param expr The expression to compile.
* @return The compiled {@link AstExpression}.
* @throws CompilationException if the expression compilation fails.
*/
public static AstExpression newAstExpression(Env env, Expression expr)
public static AstExpression newAstExpression(CelCompiler cel, Expression expr)
throws CompilationException {
Env.AstIssuesTuple astIssuesTuple = env.compile(expr.expression);
if (astIssuesTuple.hasIssues()) {
CelValidationResult compileResult = cel.compile(expr.expression);
if (!compileResult.getAllIssues().isEmpty()) {
throw new CompilationException(
"Failed to compile expression " + expr.id + ":\n" + astIssuesTuple.getIssues());
"Failed to compile expression " + expr.id + ":\n" + compileResult.getIssueString());
}
Ast ast = astIssuesTuple.getAst();
Type outType = ast.getResultType();
if (outType.getPrimitive() != Type.PrimitiveType.BOOL
&& outType.getPrimitive() != Type.PrimitiveType.STRING) {
CelAbstractSyntaxTree ast;
try {
ast = compileResult.getAst();
} catch (CelValidationException e) {
// This will not happen as we checked for issues, and it only throws when
// it has at least one issue of error severity.
throw new CompilationException(
"Failed to compile expression " + expr.id + ":\n" + compileResult.getIssueString());
}
CelKind outKind = ast.getResultType().kind();
if (outKind != CelKind.BOOL && outKind != CelKind.STRING) {
throw new CompilationException(
String.format(
"Expression outputs, wanted either bool or string: %s %s", expr.id, outType));
"Expression outputs, wanted either bool or string: %s %s", expr.id, outKind));
}
return new AstExpression(ast, expr);
}
Expand Down
5 changes: 3 additions & 2 deletions src/main/java/build/buf/protovalidate/CelPrograms.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package build.buf.protovalidate;

import build.buf.protovalidate.exceptions.ExecutionException;
import dev.cel.runtime.CelVariableResolver;
import java.util.ArrayList;
import java.util.List;
import org.jspecify.annotations.Nullable;
Expand Down Expand Up @@ -44,10 +45,10 @@ public boolean tautology() {
@Override
public List<RuleViolation.Builder> evaluate(Value val, boolean failFast)
throws ExecutionException {
Variable activation = Variable.newThisVariable(val.value(Object.class));
CelVariableResolver bindings = Variable.newThisVariable(val.value(Object.class));
List<RuleViolation.Builder> violations = new ArrayList<>();
for (CompiledProgram program : programs) {
RuleViolation.Builder violation = program.eval(val, activation);
RuleViolation.Builder violation = program.eval(val, bindings);
if (violation != null) {
violations.add(violation);
if (failFast) {
Expand Down
42 changes: 28 additions & 14 deletions src/main/java/build/buf/protovalidate/CompiledProgram.java
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@

import build.buf.protovalidate.exceptions.ExecutionException;
import build.buf.validate.FieldPath;
import dev.cel.runtime.CelEvaluationException;
import dev.cel.runtime.CelRuntime.Program;
import dev.cel.runtime.CelVariableResolver;
import org.jspecify.annotations.Nullable;
import org.projectnessie.cel.Program;
import org.projectnessie.cel.common.types.Err;
import org.projectnessie.cel.common.types.ref.Val;

/**
* {@link CompiledProgram} is a parsed and type-checked {@link Program} along with the source {@link
Expand All @@ -38,6 +38,12 @@ class CompiledProgram {
/** The rule value. */
@Nullable private final Value ruleValue;

/**
* Global variables to pass to the evaluation step. Program/CelRuntime doesn't have a concept of
* global variables.
*/
@Nullable private final CelVariableResolver globals;

/**
* Constructs a new {@link CompiledProgram}.
*
Expand All @@ -47,30 +53,38 @@ class CompiledProgram {
* @param ruleValue The rule value.
*/
public CompiledProgram(
Program program, Expression source, @Nullable FieldPath rulePath, @Nullable Value ruleValue) {
Program program,
Expression source,
@Nullable FieldPath rulePath,
@Nullable Value ruleValue,
@Nullable CelVariableResolver globals) {
this.program = program;
this.source = source;
this.rulePath = rulePath;
this.ruleValue = ruleValue;
this.globals = globals;
}

/**
* Evaluate the compiled program with a given set of {@link Variable} bindings.
* Evaluate the compiled program with a given set of {@link Variable} variables.
*
* @param bindings Variable bindings used for the evaluation.
* @param variables Variables used for the evaluation.
* @param fieldValue Field value to return in violations.
* @return The {@link build.buf.validate.Violation} from the evaluation, or null if there are no
* violations.
* @throws ExecutionException If the evaluation of the CEL program fails with an error.
*/
public RuleViolation.@Nullable Builder eval(Value fieldValue, Variable bindings)
public RuleViolation.@Nullable Builder eval(Value fieldValue, CelVariableResolver variables)
throws ExecutionException {
Program.EvalResult evalResult = program.eval(bindings);
Val val = evalResult.getVal();
if (val instanceof Err) {
throw new ExecutionException(String.format("error evaluating %s: %s", source.id, val));
Object value;
try {
if (this.globals != null) {
variables = CelVariableResolver.hierarchicalVariableResolver(variables, this.globals);
}
value = program.eval(variables);
} catch (CelEvaluationException e) {
throw new ExecutionException(String.format("error evaluating %s: %s", source.id, e));
}
Object value = val.value();
if (value instanceof String) {
if ("".equals(value)) {
return null;
Expand All @@ -88,7 +102,7 @@ public CompiledProgram(
}
return builder;
} else if (value instanceof Boolean) {
if (val.booleanValue()) {
if (Boolean.TRUE.equals(value)) {
return null;
}
RuleViolation.Builder builder =
Expand All @@ -101,7 +115,7 @@ public CompiledProgram(
}
return builder;
} else {
throw new ExecutionException(String.format("resolved to an unexpected type %s", val));
throw new ExecutionException(String.format("resolved to an unexpected type %s", value));
}
}
}
Loading