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
2 changes: 1 addition & 1 deletion gradle.properties
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Version of buf.build/bufbuild/protovalidate to use.
protovalidate.version = v1.0.0
protovalidate.version = v1.1.0

# Arguments to the protovalidate-conformance CLI
protovalidate.conformance.args = --strict_message --strict_error --expected_failures=expected-failures.yaml
Expand Down
2 changes: 2 additions & 0 deletions src/main/java/build/buf/protovalidate/DescriptorMappings.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,8 @@ final class DescriptorMappings {
EXPECTED_WKT_RULES.put("google.protobuf.Any", FIELD_RULES_DESC.findFieldByName("any"));
EXPECTED_WKT_RULES.put(
"google.protobuf.Duration", FIELD_RULES_DESC.findFieldByName("duration"));
EXPECTED_WKT_RULES.put(
"google.protobuf.FieldMask", FIELD_RULES_DESC.findFieldByName("field_mask"));
EXPECTED_WKT_RULES.put(
"google.protobuf.Timestamp", FIELD_RULES_DESC.findFieldByName("timestamp"));
}
Expand Down
43 changes: 34 additions & 9 deletions src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import org.jspecify.annotations.Nullable;

/** A build-through cache of message evaluators keyed off the provided descriptor. */
Expand All @@ -50,6 +52,10 @@ final class EvaluatorBuilder {
FieldPathUtils.fieldPathElement(
FieldRules.getDescriptor().findFieldByNumber(FieldRules.CEL_FIELD_NUMBER));

private static final FieldPathElement CEL_EXPRESSION_FIELD_PATH_ELEMENT =
FieldPathUtils.fieldPathElement(
FieldRules.getDescriptor().findFieldByNumber(FieldRules.CEL_EXPRESSION_FIELD_NUMBER));

private volatile Map<Descriptor, MessageEvaluator> evaluatorCache = Collections.emptyMap();

private final Cel cel;
Expand Down Expand Up @@ -187,7 +193,11 @@ private void buildMessage(Descriptor desc, MessageEvaluator msgEval)
private void processMessageExpressions(
Descriptor desc, MessageRules msgRules, MessageEvaluator msgEval, DynamicMessage message)
throws CompilationException {
List<Rule> celList = msgRules.getCelList();
List<Rule> celList =
Stream.concat(
expressionsToRules(msgRules.getCelExpressionList()).stream(),
msgRules.getCelList().stream())
.collect(Collectors.toList());
if (celList.isEmpty()) {
return;
}
Expand All @@ -196,7 +206,7 @@ private void processMessageExpressions(
.addMessageTypes(message.getDescriptorForType())
.addVar(Variable.THIS_NAME, StructTypeReference.create(desc.getFullName()))
.build();
List<CompiledProgram> compiledPrograms = compileRules(celList, finalCel, false);
List<CompiledProgram> compiledPrograms = compileRules(celList, finalCel, null);
if (compiledPrograms.isEmpty()) {
throw new CompilationException("compile returned null");
}
Expand Down Expand Up @@ -354,7 +364,8 @@ private void processFieldExpressions(
FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval)
throws CompilationException {
List<Rule> rulesCelList = fieldRules.getCelList();
if (rulesCelList.isEmpty()) {
List<String> exprList = fieldRules.getCelExpressionList();
if (rulesCelList.isEmpty() && exprList.isEmpty()) {
return;
}
CelBuilder builder = cel.toCelBuilder();
Expand All @@ -367,7 +378,16 @@ private void processFieldExpressions(
builder = builder.addMessageTypes(fieldDescriptor.getMessageType());
}
Cel finalCel = builder.build();
List<CompiledProgram> compiledPrograms = compileRules(rulesCelList, finalCel, true);
List<CompiledProgram> compiledPrograms = new ArrayList<>();
if (!rulesCelList.isEmpty()) {
compiledPrograms.addAll(compileRules(rulesCelList, finalCel, CEL_FIELD_PATH_ELEMENT));
}
if (!exprList.isEmpty()) {
compiledPrograms.addAll(
compileRules(
expressionsToRules(exprList), finalCel, CEL_EXPRESSION_FIELD_PATH_ELEMENT));
}

if (!compiledPrograms.isEmpty()) {
valueEvaluatorEval.append(new CelPrograms(valueEvaluatorEval, compiledPrograms));
}
Expand Down Expand Up @@ -510,19 +530,18 @@ private void processRepeatedRules(
valueEvaluatorEval.append(listEval);
}

private static List<CompiledProgram> compileRules(List<Rule> rules, Cel cel, boolean isField)
private static List<CompiledProgram> compileRules(
List<Rule> rules, Cel cel, @Nullable FieldPathElement fieldPathElement)
throws CompilationException {
List<Expression> expressions = Expression.fromRules(rules);
List<CompiledProgram> compiledPrograms = new ArrayList<>();
for (int i = 0; i < expressions.size(); i++) {
Expression expression = expressions.get(i);
AstExpression astExpression = AstExpression.newAstExpression(cel, expression);
@Nullable FieldPath rulePath = null;
if (isField) {
if (fieldPathElement != null) {
rulePath =
FieldPath.newBuilder()
.addElements(CEL_FIELD_PATH_ELEMENT.toBuilder().setIndex(i))
.build();
FieldPath.newBuilder().addElements(fieldPathElement.toBuilder().setIndex(i)).build();
}
try {
compiledPrograms.add(
Expand All @@ -538,5 +557,11 @@ private static List<CompiledProgram> compileRules(List<Rule> rules, Cel cel, boo
}
return compiledPrograms;
}

private static List<Rule> expressionsToRules(List<String> expressions) {
return expressions.stream()
.map(expr -> Rule.newBuilder().setId(expr).setExpression(expr).build())
.collect(Collectors.toList());
}
}
}
176 changes: 174 additions & 2 deletions src/main/resources/buf/validate/validate.proto
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ package buf.validate;

import "google/protobuf/descriptor.proto";
import "google/protobuf/duration.proto";
import "google/protobuf/field_mask.proto";
import "google/protobuf/timestamp.proto";

option go_package = "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate";
Expand Down Expand Up @@ -109,6 +110,25 @@ message Rule {
// MessageRules represents validation rules that are applied to the entire message.
// It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules.
message MessageRules {
// `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation
// rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax.
//
// This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for
// simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will
// be same as the `expression`.
//
// For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
//
// ```proto
// message MyMessage {
// // The field `foo` must be greater than 42.
// option (buf.validate.message).cel_expression = "this.foo > 42";
// // The field `foo` must be less than 84.
// option (buf.validate.message).cel_expression = "this.foo < 84";
// optional int32 foo = 1;
// }
// ```
repeated string cel_expression = 5;
// `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.
// These rules are written in Common Expression Language (CEL) syntax. For more information,
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
Expand Down Expand Up @@ -201,6 +221,22 @@ message OneofRules {
// FieldRules encapsulates the rules for each type of field. Depending on
// the field, the correct set should be used to ensure proper validations.
message FieldRules {
// `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation
// rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax.
//
// This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for
// simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will
// be same as the `expression`.
//
// For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
//
// ```proto
// message MyMessage {
// // The field `value` must be greater than 42.
// optional int32 value = 1 [(buf.validate.field).cel_expression = "this > 42"];
// }
// ```
repeated string cel_expression = 29;
// `cel` is a repeated field used to represent a textual expression
// in the Common Expression Language (CEL) syntax. For more information,
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
Expand Down Expand Up @@ -313,6 +349,7 @@ message FieldRules {
// Well-Known Field Types
AnyRules any = 20;
DurationRules duration = 21;
FieldMaskRules field_mask = 28;
TimestampRules timestamp = 22;
}

Expand Down Expand Up @@ -3731,6 +3768,29 @@ message StringRules {
}
];

// `ulid` specifies that the field value must be a valid ULID (Universally Unique
// Lexicographically Sortable Identifier) as defined by the [ULID specification](https://github.com/ulid/spec).
// If the field value isn't a valid ULID, an error message will be generated.
//
// ```proto
// message MyString {
// // value must be a valid ULID
// string value = 1 [(buf.validate.field).string.ulid = true];
// }
// ```
bool ulid = 35 [
(predefined).cel = {
id: "string.ulid"
message: "value must be a valid ULID"
expression: "!rules.ulid || this == '' || this.matches('^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$')"
},
(predefined).cel = {
id: "string.ulid_empty"
message: "value is empty, which is not a valid ULID"
expression: "!rules.ulid || this != ''"
}
];

// `well_known_regex` specifies a common well-known pattern
// defined as a regex. If the field value doesn't match the well-known
// regex, an error message will be generated.
Expand Down Expand Up @@ -3943,7 +4003,7 @@ message BytesRules {
// the string.
// If the field value doesn't meet the requirement, an error message is generated.
//
// ```protobuf
// ```proto
// message MyBytes {
// // value does not contain \x02\x03
// optional bytes value = 1 [(buf.validate.field).bytes.contains = "\x02\x03"];
Expand All @@ -3958,7 +4018,7 @@ message BytesRules {
// values. If the field value doesn't match any of the specified values, an
// error message is generated.
//
// ```protobuf
// ```proto
// message MyBytes {
// // value must in ["\x01\x02", "\x02\x03", "\x03\x04"]
// optional bytes value = 1 [(buf.validate.field).bytes.in = {"\x01\x02", "\x02\x03", "\x03\x04"}];
Expand Down Expand Up @@ -4052,6 +4112,31 @@ message BytesRules {
expression: "!rules.ipv6 || this.size() != 0"
}
];

// `uuid` ensures that the field `value` encodes the 128-bit UUID data as
// defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2).
// The field must contain exactly 16 bytes
// representing the UUID. If the field value isn't a valid UUID, an error
// message will be generated.
//
// ```proto
// message MyBytes {
// // value must be a valid UUID
// optional bytes value = 1 [(buf.validate.field).bytes.uuid = true];
// }
// ```
bool uuid = 15 [
(predefined).cel = {
id: "bytes.uuid"
message: "value must be a valid UUID"
expression: "!rules.uuid || this.size() == 0 || this.size() == 16"
},
(predefined).cel = {
id: "bytes.uuid_empty"
message: "value is empty, which is not a valid UUID"
expression: "!rules.uuid || this.size() != 0"
}
];
}

// `example` specifies values that the field may have. These values SHOULD
Expand Down Expand Up @@ -4605,6 +4690,93 @@ message DurationRules {
extensions 1000 to max;
}

// FieldMaskRules describe rules applied exclusively to the `google.protobuf.FieldMask` well-known type.
message FieldMaskRules {
// `const` dictates that the field must match the specified value of the `google.protobuf.FieldMask` type exactly.
// If the field's value deviates from the specified value, an error message
// will be generated.
//
// ```proto
// message MyFieldMask {
// // value must equal ["a"]
// google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask.const = {
// paths: ["a"]
// }];
// }
// ```
optional google.protobuf.FieldMask const = 1 [(predefined).cel = {
id: "field_mask.const"
expression: "this.paths != getField(rules, 'const').paths ? 'value must equal paths %s'.format([getField(rules, 'const').paths]) : ''"
}];

// `in` requires the field value to only contain paths matching specified
// values or their subpaths.
// If any of the field value's paths doesn't match the rule,
// an error message is generated.
// See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask
//
// ```proto
// message MyFieldMask {
// // The `value` FieldMask must only contain paths listed in `in`.
// google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = {
// in: ["a", "b", "c.a"]
// }];
// }
// ```
repeated string in = 2 [(predefined).cel = {
id: "field_mask.in"
expression: "!this.paths.all(p, p in getField(rules, 'in') || getField(rules, 'in').exists(f, p.startsWith(f+'.'))) ? 'value must only contain paths in %s'.format([getField(rules, 'in')]) : ''"
}];

// `not_in` requires the field value to not contain paths matching specified
// values or their subpaths.
// If any of the field value's paths matches the rule,
// an error message is generated.
// See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask
//
// ```proto
// message MyFieldMask {
// // The `value` FieldMask shall not contain paths listed in `not_in`.
// google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = {
// not_in: ["forbidden", "immutable", "c.a"]
// }];
// }
// ```
repeated string not_in = 3 [(predefined).cel = {
id: "field_mask.not_in"
expression: "!this.paths.all(p, !(p in getField(rules, 'not_in') || getField(rules, 'not_in').exists(f, p.startsWith(f+'.')))) ? 'value must not contain any paths in %s'.format([getField(rules, 'not_in')]) : ''"
}];

// `example` specifies values that the field may have. These values SHOULD
// conform to other rules. `example` values will not impact validation
// but may be used as helpful guidance on how to populate the given field.
//
// ```proto
// message MyFieldMask {
// google.protobuf.FieldMask value = 1 [
// (buf.validate.field).field_mask.example = { paths: ["a", "b"] },
// (buf.validate.field).field_mask.example = { paths: ["c.a", "d"] },
// ];
// }
// ```
repeated google.protobuf.FieldMask example = 4 [(predefined).cel = {
id: "field_mask.example"
expression: "true"
}];

// Extension fields in this range that have the (buf.validate.predefined)
// option set will be treated as predefined field rules that can then be
// set on the field options of other fields to apply field rules.
// Extension numbers 1000 to 99999 are reserved for extension numbers that are
// defined in the [Protobuf Global Extension Registry][1]. Extension numbers
// above this range are reserved for extension numbers that are not explicitly
// assigned. For rules defined in publicly-consumed schemas, use of extensions
// above 99999 is discouraged due to the risk of conflicts.
//
// [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md
extensions 1000 to max;
}

// TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type.
message TimestampRules {
// `const` dictates that this field, of the `google.protobuf.Timestamp` type, must exactly match the specified value. If the field value doesn't correspond to the specified timestamp, an error message will be generated.
Expand Down