Skip to content

Commit 0cd9449

Browse files
authored
Update protovalidate (#384)
Signed-off-by: Sri Krishna <[email protected]>
1 parent cf8460e commit 0cd9449

File tree

4 files changed

+211
-12
lines changed

4 files changed

+211
-12
lines changed

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
# Version of buf.build/bufbuild/protovalidate to use.
2-
protovalidate.version = v1.0.0
2+
protovalidate.version = v1.1.0
33

44
# Arguments to the protovalidate-conformance CLI
55
protovalidate.conformance.args = --strict_message --strict_error --expected_failures=expected-failures.yaml

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,8 @@ final class DescriptorMappings {
9292
EXPECTED_WKT_RULES.put("google.protobuf.Any", FIELD_RULES_DESC.findFieldByName("any"));
9393
EXPECTED_WKT_RULES.put(
9494
"google.protobuf.Duration", FIELD_RULES_DESC.findFieldByName("duration"));
95+
EXPECTED_WKT_RULES.put(
96+
"google.protobuf.FieldMask", FIELD_RULES_DESC.findFieldByName("field_mask"));
9597
EXPECTED_WKT_RULES.put(
9698
"google.protobuf.Timestamp", FIELD_RULES_DESC.findFieldByName("timestamp"));
9799
}

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@
4242
import java.util.Map;
4343
import java.util.Objects;
4444
import java.util.Set;
45+
import java.util.stream.Collectors;
46+
import java.util.stream.Stream;
4547
import org.jspecify.annotations.Nullable;
4648

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

55+
private static final FieldPathElement CEL_EXPRESSION_FIELD_PATH_ELEMENT =
56+
FieldPathUtils.fieldPathElement(
57+
FieldRules.getDescriptor().findFieldByNumber(FieldRules.CEL_EXPRESSION_FIELD_NUMBER));
58+
5359
private volatile Map<Descriptor, MessageEvaluator> evaluatorCache = Collections.emptyMap();
5460

5561
private final Cel cel;
@@ -187,7 +193,11 @@ private void buildMessage(Descriptor desc, MessageEvaluator msgEval)
187193
private void processMessageExpressions(
188194
Descriptor desc, MessageRules msgRules, MessageEvaluator msgEval, DynamicMessage message)
189195
throws CompilationException {
190-
List<Rule> celList = msgRules.getCelList();
196+
List<Rule> celList =
197+
Stream.concat(
198+
expressionsToRules(msgRules.getCelExpressionList()).stream(),
199+
msgRules.getCelList().stream())
200+
.collect(Collectors.toList());
191201
if (celList.isEmpty()) {
192202
return;
193203
}
@@ -196,7 +206,7 @@ private void processMessageExpressions(
196206
.addMessageTypes(message.getDescriptorForType())
197207
.addVar(Variable.THIS_NAME, StructTypeReference.create(desc.getFullName()))
198208
.build();
199-
List<CompiledProgram> compiledPrograms = compileRules(celList, finalCel, false);
209+
List<CompiledProgram> compiledPrograms = compileRules(celList, finalCel, null);
200210
if (compiledPrograms.isEmpty()) {
201211
throw new CompilationException("compile returned null");
202212
}
@@ -354,7 +364,8 @@ private void processFieldExpressions(
354364
FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval)
355365
throws CompilationException {
356366
List<Rule> rulesCelList = fieldRules.getCelList();
357-
if (rulesCelList.isEmpty()) {
367+
List<String> exprList = fieldRules.getCelExpressionList();
368+
if (rulesCelList.isEmpty() && exprList.isEmpty()) {
358369
return;
359370
}
360371
CelBuilder builder = cel.toCelBuilder();
@@ -367,7 +378,16 @@ private void processFieldExpressions(
367378
builder = builder.addMessageTypes(fieldDescriptor.getMessageType());
368379
}
369380
Cel finalCel = builder.build();
370-
List<CompiledProgram> compiledPrograms = compileRules(rulesCelList, finalCel, true);
381+
List<CompiledProgram> compiledPrograms = new ArrayList<>();
382+
if (!rulesCelList.isEmpty()) {
383+
compiledPrograms.addAll(compileRules(rulesCelList, finalCel, CEL_FIELD_PATH_ELEMENT));
384+
}
385+
if (!exprList.isEmpty()) {
386+
compiledPrograms.addAll(
387+
compileRules(
388+
expressionsToRules(exprList), finalCel, CEL_EXPRESSION_FIELD_PATH_ELEMENT));
389+
}
390+
371391
if (!compiledPrograms.isEmpty()) {
372392
valueEvaluatorEval.append(new CelPrograms(valueEvaluatorEval, compiledPrograms));
373393
}
@@ -510,19 +530,18 @@ private void processRepeatedRules(
510530
valueEvaluatorEval.append(listEval);
511531
}
512532

513-
private static List<CompiledProgram> compileRules(List<Rule> rules, Cel cel, boolean isField)
533+
private static List<CompiledProgram> compileRules(
534+
List<Rule> rules, Cel cel, @Nullable FieldPathElement fieldPathElement)
514535
throws CompilationException {
515536
List<Expression> expressions = Expression.fromRules(rules);
516537
List<CompiledProgram> compiledPrograms = new ArrayList<>();
517538
for (int i = 0; i < expressions.size(); i++) {
518539
Expression expression = expressions.get(i);
519540
AstExpression astExpression = AstExpression.newAstExpression(cel, expression);
520541
@Nullable FieldPath rulePath = null;
521-
if (isField) {
542+
if (fieldPathElement != null) {
522543
rulePath =
523-
FieldPath.newBuilder()
524-
.addElements(CEL_FIELD_PATH_ELEMENT.toBuilder().setIndex(i))
525-
.build();
544+
FieldPath.newBuilder().addElements(fieldPathElement.toBuilder().setIndex(i)).build();
526545
}
527546
try {
528547
compiledPrograms.add(
@@ -538,5 +557,11 @@ private static List<CompiledProgram> compileRules(List<Rule> rules, Cel cel, boo
538557
}
539558
return compiledPrograms;
540559
}
560+
561+
private static List<Rule> expressionsToRules(List<String> expressions) {
562+
return expressions.stream()
563+
.map(expr -> Rule.newBuilder().setId(expr).setExpression(expr).build())
564+
.collect(Collectors.toList());
565+
}
541566
}
542567
}

src/main/resources/buf/validate/validate.proto

Lines changed: 174 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package buf.validate;
1818

1919
import "google/protobuf/descriptor.proto";
2020
import "google/protobuf/duration.proto";
21+
import "google/protobuf/field_mask.proto";
2122
import "google/protobuf/timestamp.proto";
2223

2324
option go_package = "buf.build/gen/go/bufbuild/protovalidate/protocolbuffers/go/buf/validate";
@@ -109,6 +110,25 @@ message Rule {
109110
// MessageRules represents validation rules that are applied to the entire message.
110111
// It includes disabling options and a list of Rule messages representing Common Expression Language (CEL) validation rules.
111112
message MessageRules {
113+
// `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation
114+
// rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax.
115+
//
116+
// This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for
117+
// simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will
118+
// be same as the `expression`.
119+
//
120+
// For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
121+
//
122+
// ```proto
123+
// message MyMessage {
124+
// // The field `foo` must be greater than 42.
125+
// option (buf.validate.message).cel_expression = "this.foo > 42";
126+
// // The field `foo` must be less than 84.
127+
// option (buf.validate.message).cel_expression = "this.foo < 84";
128+
// optional int32 foo = 1;
129+
// }
130+
// ```
131+
repeated string cel_expression = 5;
112132
// `cel` is a repeated field of type Rule. Each Rule specifies a validation rule to be applied to this message.
113133
// These rules are written in Common Expression Language (CEL) syntax. For more information,
114134
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
@@ -201,6 +221,22 @@ message OneofRules {
201221
// FieldRules encapsulates the rules for each type of field. Depending on
202222
// the field, the correct set should be used to ensure proper validations.
203223
message FieldRules {
224+
// `cel_expression` is a repeated field CEL expressions. Each expression specifies a validation
225+
// rule to be applied to this message. These rules are written in Common Expression Language (CEL) syntax.
226+
//
227+
// This is a simplified form of the `cel` Rule field, where only `expression` is set. This allows for
228+
// simpler syntax when defining CEL Rules where `id` and `message` derived from the `expression`. `id` will
229+
// be same as the `expression`.
230+
//
231+
// For more information, [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
232+
//
233+
// ```proto
234+
// message MyMessage {
235+
// // The field `value` must be greater than 42.
236+
// optional int32 value = 1 [(buf.validate.field).cel_expression = "this > 42"];
237+
// }
238+
// ```
239+
repeated string cel_expression = 29;
204240
// `cel` is a repeated field used to represent a textual expression
205241
// in the Common Expression Language (CEL) syntax. For more information,
206242
// [see our documentation](https://buf.build/docs/protovalidate/schemas/custom-rules/).
@@ -313,6 +349,7 @@ message FieldRules {
313349
// Well-Known Field Types
314350
AnyRules any = 20;
315351
DurationRules duration = 21;
352+
FieldMaskRules field_mask = 28;
316353
TimestampRules timestamp = 22;
317354
}
318355

@@ -3731,6 +3768,29 @@ message StringRules {
37313768
}
37323769
];
37333770

3771+
// `ulid` specifies that the field value must be a valid ULID (Universally Unique
3772+
// Lexicographically Sortable Identifier) as defined by the [ULID specification](https://github.com/ulid/spec).
3773+
// If the field value isn't a valid ULID, an error message will be generated.
3774+
//
3775+
// ```proto
3776+
// message MyString {
3777+
// // value must be a valid ULID
3778+
// string value = 1 [(buf.validate.field).string.ulid = true];
3779+
// }
3780+
// ```
3781+
bool ulid = 35 [
3782+
(predefined).cel = {
3783+
id: "string.ulid"
3784+
message: "value must be a valid ULID"
3785+
expression: "!rules.ulid || this == '' || this.matches('^[0-7][0-9A-HJKMNP-TV-Za-hjkmnp-tv-z]{25}$')"
3786+
},
3787+
(predefined).cel = {
3788+
id: "string.ulid_empty"
3789+
message: "value is empty, which is not a valid ULID"
3790+
expression: "!rules.ulid || this != ''"
3791+
}
3792+
];
3793+
37343794
// `well_known_regex` specifies a common well-known pattern
37353795
// defined as a regex. If the field value doesn't match the well-known
37363796
// regex, an error message will be generated.
@@ -3943,7 +4003,7 @@ message BytesRules {
39434003
// the string.
39444004
// If the field value doesn't meet the requirement, an error message is generated.
39454005
//
3946-
// ```protobuf
4006+
// ```proto
39474007
// message MyBytes {
39484008
// // value does not contain \x02\x03
39494009
// optional bytes value = 1 [(buf.validate.field).bytes.contains = "\x02\x03"];
@@ -3958,7 +4018,7 @@ message BytesRules {
39584018
// values. If the field value doesn't match any of the specified values, an
39594019
// error message is generated.
39604020
//
3961-
// ```protobuf
4021+
// ```proto
39624022
// message MyBytes {
39634023
// // value must in ["\x01\x02", "\x02\x03", "\x03\x04"]
39644024
// optional bytes value = 1 [(buf.validate.field).bytes.in = {"\x01\x02", "\x02\x03", "\x03\x04"}];
@@ -4052,6 +4112,31 @@ message BytesRules {
40524112
expression: "!rules.ipv6 || this.size() != 0"
40534113
}
40544114
];
4115+
4116+
// `uuid` ensures that the field `value` encodes the 128-bit UUID data as
4117+
// defined by [RFC 4122](https://datatracker.ietf.org/doc/html/rfc4122#section-4.1.2).
4118+
// The field must contain exactly 16 bytes
4119+
// representing the UUID. If the field value isn't a valid UUID, an error
4120+
// message will be generated.
4121+
//
4122+
// ```proto
4123+
// message MyBytes {
4124+
// // value must be a valid UUID
4125+
// optional bytes value = 1 [(buf.validate.field).bytes.uuid = true];
4126+
// }
4127+
// ```
4128+
bool uuid = 15 [
4129+
(predefined).cel = {
4130+
id: "bytes.uuid"
4131+
message: "value must be a valid UUID"
4132+
expression: "!rules.uuid || this.size() == 0 || this.size() == 16"
4133+
},
4134+
(predefined).cel = {
4135+
id: "bytes.uuid_empty"
4136+
message: "value is empty, which is not a valid UUID"
4137+
expression: "!rules.uuid || this.size() != 0"
4138+
}
4139+
];
40554140
}
40564141

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

4693+
// FieldMaskRules describe rules applied exclusively to the `google.protobuf.FieldMask` well-known type.
4694+
message FieldMaskRules {
4695+
// `const` dictates that the field must match the specified value of the `google.protobuf.FieldMask` type exactly.
4696+
// If the field's value deviates from the specified value, an error message
4697+
// will be generated.
4698+
//
4699+
// ```proto
4700+
// message MyFieldMask {
4701+
// // value must equal ["a"]
4702+
// google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask.const = {
4703+
// paths: ["a"]
4704+
// }];
4705+
// }
4706+
// ```
4707+
optional google.protobuf.FieldMask const = 1 [(predefined).cel = {
4708+
id: "field_mask.const"
4709+
expression: "this.paths != getField(rules, 'const').paths ? 'value must equal paths %s'.format([getField(rules, 'const').paths]) : ''"
4710+
}];
4711+
4712+
// `in` requires the field value to only contain paths matching specified
4713+
// values or their subpaths.
4714+
// If any of the field value's paths doesn't match the rule,
4715+
// an error message is generated.
4716+
// See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask
4717+
//
4718+
// ```proto
4719+
// message MyFieldMask {
4720+
// // The `value` FieldMask must only contain paths listed in `in`.
4721+
// google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = {
4722+
// in: ["a", "b", "c.a"]
4723+
// }];
4724+
// }
4725+
// ```
4726+
repeated string in = 2 [(predefined).cel = {
4727+
id: "field_mask.in"
4728+
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')]) : ''"
4729+
}];
4730+
4731+
// `not_in` requires the field value to not contain paths matching specified
4732+
// values or their subpaths.
4733+
// If any of the field value's paths matches the rule,
4734+
// an error message is generated.
4735+
// See: https://protobuf.dev/reference/protobuf/google.protobuf/#field-mask
4736+
//
4737+
// ```proto
4738+
// message MyFieldMask {
4739+
// // The `value` FieldMask shall not contain paths listed in `not_in`.
4740+
// google.protobuf.FieldMask value = 1 [(buf.validate.field).field_mask = {
4741+
// not_in: ["forbidden", "immutable", "c.a"]
4742+
// }];
4743+
// }
4744+
// ```
4745+
repeated string not_in = 3 [(predefined).cel = {
4746+
id: "field_mask.not_in"
4747+
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')]) : ''"
4748+
}];
4749+
4750+
// `example` specifies values that the field may have. These values SHOULD
4751+
// conform to other rules. `example` values will not impact validation
4752+
// but may be used as helpful guidance on how to populate the given field.
4753+
//
4754+
// ```proto
4755+
// message MyFieldMask {
4756+
// google.protobuf.FieldMask value = 1 [
4757+
// (buf.validate.field).field_mask.example = { paths: ["a", "b"] },
4758+
// (buf.validate.field).field_mask.example = { paths: ["c.a", "d"] },
4759+
// ];
4760+
// }
4761+
// ```
4762+
repeated google.protobuf.FieldMask example = 4 [(predefined).cel = {
4763+
id: "field_mask.example"
4764+
expression: "true"
4765+
}];
4766+
4767+
// Extension fields in this range that have the (buf.validate.predefined)
4768+
// option set will be treated as predefined field rules that can then be
4769+
// set on the field options of other fields to apply field rules.
4770+
// Extension numbers 1000 to 99999 are reserved for extension numbers that are
4771+
// defined in the [Protobuf Global Extension Registry][1]. Extension numbers
4772+
// above this range are reserved for extension numbers that are not explicitly
4773+
// assigned. For rules defined in publicly-consumed schemas, use of extensions
4774+
// above 99999 is discouraged due to the risk of conflicts.
4775+
//
4776+
// [1]: https://github.com/protocolbuffers/protobuf/blob/main/docs/options.md
4777+
extensions 1000 to max;
4778+
}
4779+
46084780
// TimestampRules describe the rules applied exclusively to the `google.protobuf.Timestamp` well-known type.
46094781
message TimestampRules {
46104782
// `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.

0 commit comments

Comments
 (0)