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 = v0.11.1
protovalidate.version = v0.12.0

# Arguments to the protovalidate-conformance CLI
protovalidate.conformance.args = --strict_message --strict_error --expected_failures=expected-failures.yaml
Expand Down
33 changes: 33 additions & 0 deletions src/main/java/build/buf/protovalidate/EvaluatorBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -379,16 +379,38 @@ private void processEmbeddedMessage(
private void processWrapperRules(
FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval)
throws CompilationException {

if (fieldDescriptor.getJavaType() != FieldDescriptor.JavaType.MESSAGE
|| fieldDescriptor.isMapField()
|| (fieldDescriptor.isRepeated() && !valueEvaluatorEval.hasNestedRule())) {
return;
}
FieldDescriptor expectedWrapperDescriptor =
DescriptorMappings.expectedWrapperRules(fieldDescriptor.getMessageType().getFullName());

// Verify that the expected wrapper rules for this field are equal to the rules specified on
// the field
if (expectedWrapperDescriptor != null) {
FieldDescriptor oneofFieldDescriptor =
fieldRules.getOneofFieldDescriptor(DescriptorMappings.FIELD_RULES_ONEOF_DESC);
// If there are no field rules set, just return
if (oneofFieldDescriptor == null) {
return;
}
if (!expectedWrapperDescriptor
.getMessageType()
.getFullName()
.equals(oneofFieldDescriptor.getMessageType().getFullName())) {
throw new CompilationException(
String.format(
"mismatched message rules, %s is not a valid rule for field %s",
oneofFieldDescriptor.getName(), fieldDescriptor.getName()));
}
}
if (expectedWrapperDescriptor == null || !fieldRules.hasField(expectedWrapperDescriptor)) {
return;
}

ValueEvaluator unwrapped =
new ValueEvaluator(
valueEvaluatorEval.getDescriptor(), valueEvaluatorEval.getNestedRule());
Expand All @@ -399,6 +421,17 @@ private void processWrapperRules(
private void processStandardRules(
FieldDescriptor fieldDescriptor, FieldRules fieldRules, ValueEvaluator valueEvaluatorEval)
throws CompilationException {

// If this is a wrapper field, just return. Wrapper fields are handled by
// processWrapperRules and their unwrapped values are passed through the process gauntlet.
if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
FieldDescriptor expectedWrapperDescriptor =
DescriptorMappings.expectedWrapperRules(fieldDescriptor.getMessageType().getFullName());
if (expectedWrapperDescriptor != null) {
return;
}
}

List<CompiledProgram> compile =
ruleCache.compile(fieldDescriptor, fieldRules, valueEvaluatorEval.hasNestedRule());
if (compile.isEmpty()) {
Expand Down
15 changes: 13 additions & 2 deletions src/main/java/build/buf/protovalidate/RuleCache.java
Original file line number Diff line number Diff line change
Expand Up @@ -290,6 +290,7 @@ private ResolvedRule resolveRules(
// indicating whether it is for items.
FieldDescriptor expectedRuleDescriptor =
DescriptorMappings.getExpectedRuleDescriptor(fieldDescriptor, forItems);

if (expectedRuleDescriptor != null
&& !oneofFieldDescriptor.getFullName().equals(expectedRuleDescriptor.getFullName())) {
// If the expected rule does not match the actual oneof rule, throw a
Expand All @@ -303,9 +304,19 @@ private ResolvedRule resolveRules(
}

// If the expected rule descriptor is null or if the field rules do not have the
// oneof field descriptor
// there are no rules to resolve, so return null.
// oneof field descriptor there are no rules to resolve, so return null.
if (expectedRuleDescriptor == null || !fieldRules.hasField(oneofFieldDescriptor)) {
if (expectedRuleDescriptor == null) {
// The only expected rule descriptor for message fields is for well known types.
// If we didn't find a descriptor and this is a message, there must be a mismatch.
if (fieldDescriptor.getJavaType() == FieldDescriptor.JavaType.MESSAGE) {
throw new CompilationException(
String.format(
"mismatched message rules, %s is not a valid rule for field %s",
oneofFieldDescriptor.getName(), fieldDescriptor.getName()));
}
}

return null;
}

Expand Down
25 changes: 19 additions & 6 deletions src/main/resources/buf/validate/validate.proto
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,7 @@ message FieldRules {
// - proto2 scalar fields (both optional and required)
// - proto3 scalar fields must be non-zero to be considered populated
// - repeated and map fields must be non-empty to be considered populated
// - map keys/values and repeated items are always considered populated
//
// ```proto
// message MyMessage {
Expand Down Expand Up @@ -625,7 +626,7 @@ message FloatRules {
// message MyFloat {
// float value = 1 [
// (buf.validate.field).float.example = 1.0,
// (buf.validate.field).float.example = "Infinity"
// (buf.validate.field).float.example = inf
// ];
// }
// ```
Expand Down Expand Up @@ -846,7 +847,7 @@ message DoubleRules {
// message MyDouble {
// double value = 1 [
// (buf.validate.field).double.example = 1.0,
// (buf.validate.field).double.example = "Infinity"
// (buf.validate.field).double.example = inf
// ];
// }
// ```
Expand Down Expand Up @@ -4241,6 +4242,9 @@ message RepeatedRules {
// in the field. Even for repeated message fields, validation is executed
// against each item unless skip is explicitly specified.
//
// Note that repeated items are always considered populated. The `required`
// rule does not apply.
//
// ```proto
// message MyRepeated {
// // The items in the field `value` must follow the specified rules.
Expand Down Expand Up @@ -4299,6 +4303,9 @@ message MapRules {

//Specifies the rules to be applied to each key in the field.
//
// Note that map keys are always considered populated. The `required`
// rule does not apply.
//
// ```proto
// message MyMap {
// // The keys in the field `value` must follow the specified rules.
Expand All @@ -4316,6 +4323,9 @@ message MapRules {
// field. Message values will still have their validations evaluated unless
//skip is specified here.
//
// Note that map values are always considered populated. The `required`
// rule does not apply.
//
// ```proto
// message MyMap {
// // The values in the field `value` must follow the specified rules.
Expand Down Expand Up @@ -4351,7 +4361,9 @@ message AnyRules {
// ```proto
// message MyAny {
// // The `value` field must have a `type_url` equal to one of the specified values.
// google.protobuf.Any value = 1 [(buf.validate.field).any.in = ["type.googleapis.com/MyType1", "type.googleapis.com/MyType2"]];
// google.protobuf.Any value = 1 [(buf.validate.field).any = {
// in: ["type.googleapis.com/MyType1", "type.googleapis.com/MyType2"]
// }];
// }
// ```
repeated string in = 2;
Expand All @@ -4360,8 +4372,10 @@ message AnyRules {
//
// ```proto
// message MyAny {
// // The field `value` must not have a `type_url` equal to any of the specified values.
// google.protobuf.Any value = 1 [(buf.validate.field).any.not_in = ["type.googleapis.com/ForbiddenType1", "type.googleapis.com/ForbiddenType2"]];
// // The `value` field must not have a `type_url` equal to any of the specified values.
// google.protobuf.Any value = 1 [(buf.validate.field).any = {
// not_in: ["type.googleapis.com/ForbiddenType1", "type.googleapis.com/ForbiddenType2"]
// }];
// }
// ```
repeated string not_in = 3;
Expand Down Expand Up @@ -4783,7 +4797,6 @@ message TimestampRules {
// ];
// }
// ```

repeated google.protobuf.Timestamp example = 10 [(predefined).cel = {
id: "timestamp.example"
expression: "true"
Expand Down
2 changes: 1 addition & 1 deletion src/test/java/build/buf/protovalidate/FormatTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ class FormatTest {
private static List<SimpleTest> formatErrorTests;

@BeforeAll
private static void setUp() throws Exception {
public static void setUp() throws Exception {
Copy link
Contributor Author

Choose a reason for hiding this comment

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

All of a sudden this warning was showing when running tests:

Gradle Test Executor 18 STANDARD_ERROR
    Jun 03, 2025 3:44:38 PM org.junit.platform.launcher.core.DiscoveryIssueNotifier logIssues
    WARNING: TestEngine with ID 'junit-jupiter' encountered a non-critical issue during test discovery:

    (1) [WARNING] @BeforeAll method 'private static void build.buf.protovalidate.FormatTest.setUp() throws java.lang.Exception' should not be private. This will be disallowed in a future release.
        Source: MethodSource [className = 'build.buf.protovalidate.FormatTest', methodName = 'setUp', methodParameterTypes = '']
                at build.buf.protovalidate.FormatTest.setUp(SourceFile:0)

// The test data from the cel-spec conformance tests
List<SimpleTestSection> celSpecSections =
loadTestData("src/test/resources/testdata/string_ext_" + CEL_SPEC_VERSION + ".textproto");
Expand Down