diff --git a/gradle.properties b/gradle.properties index 5204de59..63e8ae66 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ # Version of buf.build/bufbuild/protovalidate to use. -protovalidate.version = v0.13.3 +protovalidate.version = v0.14.0 # Arguments to the protovalidate-conformance CLI protovalidate.conformance.args = --strict_message --strict_error --expected_failures=expected-failures.yaml diff --git a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java b/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java index 04f8e40a..ab5bd368 100644 --- a/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java +++ b/src/main/java/build/buf/protovalidate/EvaluatorBuilder.java @@ -174,9 +174,6 @@ private void buildMessage(Descriptor desc, MessageEvaluator msgEval) DynamicMessage defaultInstance = DynamicMessage.newBuilder(desc).buildPartial(); Descriptor descriptor = defaultInstance.getDescriptorForType(); MessageRules msgRules = resolver.resolveMessageRules(descriptor); - if (msgRules.getDisabled()) { - return; - } processMessageExpressions(descriptor, msgRules, msgEval, defaultInstance); processMessageOneofRules(descriptor, msgRules, msgEval); processOneofRules(descriptor, msgEval); @@ -252,7 +249,7 @@ private void processFields(Descriptor desc, MessageRules msgRules, MessageEvalua if (!fieldRules.hasIgnore() && msgRules.getOneofList().stream() .anyMatch(oneof -> oneof.getFieldsList().contains(fieldDescriptor.getName()))) { - fieldRules = fieldRules.toBuilder().setIgnore(Ignore.IGNORE_IF_UNPOPULATED).build(); + fieldRules = fieldRules.toBuilder().setIgnore(Ignore.IGNORE_IF_ZERO_VALUE).build(); } FieldEvaluator fldEval = buildField(descriptor, fieldRules); msgEval.append(fldEval); @@ -262,30 +259,19 @@ private void processFields(Descriptor desc, MessageRules msgRules, MessageEvalua private FieldEvaluator buildField(FieldDescriptor fieldDescriptor, FieldRules fieldRules) throws CompilationException { ValueEvaluator valueEvaluatorEval = new ValueEvaluator(fieldDescriptor, null); - boolean ignoreDefault = fieldDescriptor.hasPresence() && shouldIgnoreDefault(fieldRules); - Object zero = null; - if (ignoreDefault) { - zero = zeroValue(fieldDescriptor, false); - } FieldEvaluator fieldEvaluator = new FieldEvaluator( valueEvaluatorEval, fieldDescriptor, fieldRules.getRequired(), fieldDescriptor.hasPresence(), - fieldRules.getIgnore(), - zero); + fieldRules.getIgnore()); buildValue(fieldDescriptor, fieldRules, fieldEvaluator.valueEvaluator); return fieldEvaluator; } private static boolean shouldIgnoreEmpty(FieldRules rules) { - return rules.getIgnore() == Ignore.IGNORE_IF_UNPOPULATED - || rules.getIgnore() == Ignore.IGNORE_IF_DEFAULT_VALUE; - } - - private static boolean shouldIgnoreDefault(FieldRules rules) { - return rules.getIgnore() == Ignore.IGNORE_IF_DEFAULT_VALUE; + return rules.getIgnore() == Ignore.IGNORE_IF_ZERO_VALUE; } private void buildValue( diff --git a/src/main/java/build/buf/protovalidate/FieldEvaluator.java b/src/main/java/build/buf/protovalidate/FieldEvaluator.java index 5faf8a87..8b0d1ce2 100644 --- a/src/main/java/build/buf/protovalidate/FieldEvaluator.java +++ b/src/main/java/build/buf/protovalidate/FieldEvaluator.java @@ -22,8 +22,6 @@ import com.google.protobuf.Message; import java.util.Collections; import java.util.List; -import java.util.Objects; -import org.jspecify.annotations.Nullable; /** Performs validation on a single message field, defined by its descriptor. */ final class FieldEvaluator implements Evaluator { @@ -52,23 +50,19 @@ final class FieldEvaluator implements Evaluator { /** Whether the field distinguishes between unpopulated and default values. */ private final boolean hasPresence; - @Nullable private final Object zero; - /** Constructs a new {@link FieldEvaluator} */ FieldEvaluator( ValueEvaluator valueEvaluator, FieldDescriptor descriptor, boolean required, boolean hasPresence, - Ignore ignore, - @Nullable Object zero) { + Ignore ignore) { this.helper = new RuleViolationHelper(valueEvaluator); this.valueEvaluator = valueEvaluator; this.descriptor = descriptor; this.required = required; this.hasPresence = hasPresence; this.ignore = ignore; - this.zero = zero; } @Override @@ -92,17 +86,7 @@ private boolean shouldIgnoreAlways() { * set. */ private boolean shouldIgnoreEmpty() { - return this.hasPresence - || this.ignore == Ignore.IGNORE_IF_UNPOPULATED - || this.ignore == Ignore.IGNORE_IF_DEFAULT_VALUE; - } - - /** - * Returns whether a field should skip validation on its zero value, including for fields which - * have field presence and are set to the zero value. - */ - private boolean shouldIgnoreDefault() { - return this.hasPresence && this.ignore == Ignore.IGNORE_IF_DEFAULT_VALUE; + return this.hasPresence || this.ignore == Ignore.IGNORE_IF_ZERO_VALUE; } @Override @@ -134,11 +118,7 @@ public List evaluate(Value val, boolean failFast) if (this.shouldIgnoreEmpty() && !hasField) { return RuleViolation.NO_VIOLATIONS; } - Object fieldValue = message.getField(descriptor); - if (this.shouldIgnoreDefault() - && Objects.equals(zero, ProtoAdapter.toCel(descriptor, fieldValue))) { - return RuleViolation.NO_VIOLATIONS; - } - return valueEvaluator.evaluate(new ObjectValue(descriptor, fieldValue), failFast); + return valueEvaluator.evaluate( + new ObjectValue(descriptor, message.getField(descriptor)), failFast); } } diff --git a/src/main/resources/buf/validate/validate.proto b/src/main/resources/buf/validate/validate.proto index 8a0e0b44..f96431d0 100644 --- a/src/main/resources/buf/validate/validate.proto +++ b/src/main/resources/buf/validate/validate.proto @@ -109,17 +109,6 @@ 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 { - // `disabled` is a boolean flag that, when set to true, nullifies any validation rules for this message. - // This includes any fields within the message that would otherwise support validation. - // - // ```proto - // message MyMessage { - // // validation will be bypassed for this message - // option (buf.validate.message).disabled = true; - // } - // ``` - optional bool disabled = 1; - // `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/). @@ -156,7 +145,7 @@ message MessageRules { // silently ignored when unmarshalling, with only the last field being set when // unmarshalling completes. // - // Note that adding a field to a `oneof` will also set the IGNORE_IF_UNPOPULATED on the fields. This means + // Note that adding a field to a `oneof` will also set the IGNORE_IF_ZERO_VALUE on the fields. This means // only the field that is set will be validated and the unset fields are not validated according to the field rules. // This behavior can be overridden by setting `ignore` against a field. // @@ -173,6 +162,9 @@ message MessageRules { // } // ``` repeated MessageOneofRule oneof = 4; + + reserved 1; + reserved "disabled"; } message MessageOneofRule { @@ -187,9 +179,8 @@ message MessageOneofRule { // The `OneofRules` message type enables you to manage rules for // oneof fields in your protobuf messages. message OneofRules { - // If `required` is true, exactly one field of the oneof must be present. A - // validation error is returned if no fields in the oneof are present. The - // field itself may still be a default value; further rules + // If `required` is true, exactly one field of the oneof must be set. A + // validation error is returned if no fields in the oneof are set. Further rules // should be placed on the fields themselves to ensure they are valid values, // such as `min_len` or `gt`. // @@ -225,35 +216,66 @@ message FieldRules { // } // ``` repeated Rule cel = 23; - // If `required` is true, the field must be populated. A populated field can be - // described as "serialized in the wire format," which includes: - // - // - the following "nullable" fields must be explicitly set to be considered populated: - // - singular message fields (whose fields may be unpopulated/default values) - // - member fields of a oneof (may be their default value) - // - proto3 optional fields (may be their default value) - // - 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 + // If `required` is true, the field must be set. A validation error is returned + // if the field is not set. // // ```proto - // message MyMessage { - // // The field `value` must be set to a non-null value. - // optional MyOtherMessage value = 1 [(buf.validate.field).required = true]; + // syntax="proto3"; + // + // message FieldsWithPresence { + // // Requires any string to be set, including the empty string. + // optional string link = 1 [ + // (buf.validate.field).required = true + // ]; + // // Requires true or false to be set. + // optional bool disabled = 2 [ + // (buf.validate.field).required = true + // ]; + // // Requires a message to be set, including the empty message. + // SomeMessage msg = 4 [ + // (buf.validate.field).required = true + // ]; + // } + // ``` + // + // All fields in the example above track presence. By default, Protovalidate + // ignores rules on those fields if no value is set. `required` ensures that + // the fields are set and valid. + // + // Fields that don't track presence are always validated by Protovalidate, + // whether they are set or not. It is not necessary to add `required`: + // + // ```proto + // syntax="proto3"; + // + // message FieldsWithoutPresence { + // // `string.email` always applies, even to an empty string. + // string link = 1 [ + // (buf.validate.field).string.email = true + // ]; + // // `repeated.min_items` always applies, even to an empty list. + // repeated string labels = 4 [ + // (buf.validate.field).repeated.min_items = 1 + // ]; // } // ``` + // + // To learn which fields track presence, see the + // [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat). + // + // Note: While field rules can be applied to repeated items, map keys, and map + // values, the elements are always considered to be set. Consequently, + // specifying `repeated.items.required` is redundant. optional bool required = 25; - // Skip validation on the field if its value matches the specified criteria. - // See Ignore enum for details. + // Ignore validation rules on the field if its value matches the specified + // criteria. See the `Ignore` enum for details. // // ```proto // message UpdateRequest { - // // The uri rule only applies if the field is populated and not an empty - // // string. - // optional string url = 1 [ - // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE, - // (buf.validate.field).string.uri = true, + // // The uri rule only applies if the field is not an empty string. + // string url = 1 [ + // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED, + // (buf.validate.field).string.uri = true // ]; // } // ``` @@ -312,150 +334,89 @@ message PredefinedRules { repeated Rule cel = 1; reserved 24, 26; - reserved - "skipped" - "ignore_empty" -; + reserved "skipped", "ignore_empty"; } -// Specifies how FieldRules.ignore behaves. See the documentation for -// FieldRules.required for definitions of "populated" and "nullable". +// Specifies how `FieldRules.ignore` behaves, depending on the field's value, and +// whether the field tracks presence. enum Ignore { - // Validation is only skipped if it's an unpopulated nullable field. + // Ignore rules if the field tracks presence and is unset. This is the default + // behavior. + // + // In proto3, only message fields, members of a Protobuf `oneof`, and fields + // with the `optional` label track presence. Consequently, the following fields + // are always validated, whether a value is set or not: // // ```proto // syntax="proto3"; // - // message Request { - // // The uri rule applies to any value, including the empty string. - // string foo = 1 [ - // (buf.validate.field).string.uri = true - // ]; - // - // // The uri rule only applies if the field is set, including if it's - // // set to the empty string. - // optional string bar = 2 [ - // (buf.validate.field).string.uri = true + // message RulesApply { + // string email = 1 [ + // (buf.validate.field).string.email = true // ]; - // - // // The min_items rule always applies, even if the list is empty. - // repeated string baz = 3 [ - // (buf.validate.field).repeated.min_items = 3 + // int32 age = 2 [ + // (buf.validate.field).int32.gt = 0 // ]; - // - // // The custom CEL rule applies only if the field is set, including if - // // it's the "zero" value of that message. - // SomeMessage quux = 4 [ - // (buf.validate.field).cel = {/* ... */} + // repeated string labels = 3 [ + // (buf.validate.field).repeated.min_items = 1 // ]; // } // ``` - IGNORE_UNSPECIFIED = 0; - - // Validation is skipped if the field is unpopulated. This rule is redundant - // if the field is already nullable. // - // ```proto - // syntax="proto3 + // In contrast, the following fields track presence, and are only validated if + // a value is set: // - // message Request { - // // The uri rule applies only if the value is not the empty string. - // string foo = 1 [ - // (buf.validate.field).string.uri = true, - // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED - // ]; - // - // // IGNORE_IF_UNPOPULATED is equivalent to IGNORE_UNSPECIFIED in this - // // case: the uri rule only applies if the field is set, including if - // // it's set to the empty string. - // optional string bar = 2 [ - // (buf.validate.field).string.uri = true, - // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED - // ]; + // ```proto + // syntax="proto3"; // - // // The min_items rule only applies if the list has at least one item. - // repeated string baz = 3 [ - // (buf.validate.field).repeated.min_items = 3, - // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED + // message RulesApplyIfSet { + // optional string email = 1 [ + // (buf.validate.field).string.email = true // ]; - // - // // IGNORE_IF_UNPOPULATED is equivalent to IGNORE_UNSPECIFIED in this - // // case: the custom CEL rule applies only if the field is set, including - // // if it's the "zero" value of that message. - // SomeMessage quux = 4 [ - // (buf.validate.field).cel = {/* ... */}, - // (buf.validate.field).ignore = IGNORE_IF_UNPOPULATED + // oneof ref { + // string reference = 2 [ + // (buf.validate.field).string.uuid = true + // ]; + // string name = 3 [ + // (buf.validate.field).string.min_len = 4 + // ]; + // } + // SomeMessage msg = 4 [ + // (buf.validate.field).cel = {/* ... */} // ]; // } // ``` - IGNORE_IF_UNPOPULATED = 1; - - // Validation is skipped if the field is unpopulated or if it is a nullable - // field populated with its default value. This is typically the zero or - // empty value, but proto2 scalars support custom defaults. For messages, the - // default is a non-null message with all its fields unpopulated. - // - // ```proto - // syntax="proto3 - // - // message Request { - // // IGNORE_IF_DEFAULT_VALUE is equivalent to IGNORE_IF_UNPOPULATED in - // // this case; the uri rule applies only if the value is not the empty - // // string. - // string foo = 1 [ - // (buf.validate.field).string.uri = true, - // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE - // ]; // - // // The uri rule only applies if the field is set to a value other than - // // the empty string. - // optional string bar = 2 [ - // (buf.validate.field).string.uri = true, - // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE - // ]; - // - // // IGNORE_IF_DEFAULT_VALUE is equivalent to IGNORE_IF_UNPOPULATED in - // // this case; the min_items rule only applies if the list has at least - // // one item. - // repeated string baz = 3 [ - // (buf.validate.field).repeated.min_items = 3, - // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE - // ]; + // To ensure that such a field is set, add the `required` rule. // - // // The custom CEL rule only applies if the field is set to a value other - // // than an empty message (i.e., fields are unpopulated). - // SomeMessage quux = 4 [ - // (buf.validate.field).cel = {/* ... */}, - // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE - // ]; - // } - // ``` + // To learn which fields track presence, see the + // [Field Presence cheat sheet](https://protobuf.dev/programming-guides/field_presence/#cheat). + IGNORE_UNSPECIFIED = 0; + + // Ignore rules if the field is unset, or set to the zero value. // - // This rule is affected by proto2 custom default values: + // The zero value depends on the field type: + // - For strings, the zero value is the empty string. + // - For bytes, the zero value is empty bytes. + // - For bool, the zero value is false. + // - For numeric types, the zero value is zero. + // - For enums, the zero value is the first defined enum value. + // - For repeated fields, the zero is an empty list. + // - For map fields, the zero is an empty map. + // - For message fields, absence of the message (typically a null-value) is considered zero value. // - // ```proto - // syntax="proto2"; - // - // message Request { - // // The gt rule only applies if the field is set and it's value is not - // the default (i.e., not -42). The rule even applies if the field is set - // to zero since the default value differs. - // optional int32 value = 1 [ - // default = -42, - // (buf.validate.field).int32.gt = 0, - // (buf.validate.field).ignore = IGNORE_IF_DEFAULT_VALUE - // ]; - // } - IGNORE_IF_DEFAULT_VALUE = 2; + // For fields that track presence (e.g. adding the `optional` label in proto3), + // this a no-op and behavior is the same as the default `IGNORE_UNSPECIFIED`. + IGNORE_IF_ZERO_VALUE = 1; - // The validation rules of this field will be skipped and not evaluated. This - // is useful for situations that necessitate turning off the rules of a field - // containing a message that may not make sense in the current context, or to - // temporarily disable rules during development. + // Always ignore rules, including the `required` rule. + // + // This is useful for ignoring the rules of a referenced message, or to + // temporarily ignore rules during development. // // ```proto // message MyMessage { - // // The field's rules will always be ignored, including any validation's + // // The field's rules will always be ignored, including any validations // // on value's fields. // MyOtherMessage value = 1 [ // (buf.validate.field).ignore = IGNORE_ALWAYS]; @@ -463,10 +424,8 @@ enum Ignore { // ``` IGNORE_ALWAYS = 3; - reserved - "IGNORE_EMPTY" - "IGNORE_DEFAULT" -; + reserved 2; + reserved "IGNORE_EMPTY", "IGNORE_DEFAULT", "IGNORE_IF_DEFAULT_VALUE", "IGNORE_IF_UNPOPULATED"; } // FloatRules describes the rules applied to `float` values. These @@ -4285,10 +4244,7 @@ message RepeatedRules { // `items` details the rules to be applied to each item // 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. + // against each item unless `ignore` is specified. // // ```proto // message MyRepeated { @@ -4301,6 +4257,9 @@ message RepeatedRules { // }]; // } // ``` + // + // Note that the `required` rule does not apply. Repeated items + // cannot be unset. optional FieldRules items = 4; // Extension fields in this range that have the (buf.validate.predefined) @@ -4318,7 +4277,7 @@ message RepeatedRules { // MapRules describe the rules applied to `map` values. message MapRules { - //Specifies the minimum number of key-value pairs allowed. If the field has + // Specifies the minimum number of key-value pairs allowed. If the field has // fewer key-value pairs than specified, an error message is generated. // // ```proto @@ -4332,7 +4291,7 @@ message MapRules { expression: "uint(this.size()) < rules.min_pairs ? 'map must be at least %d entries'.format([rules.min_pairs]) : ''" }]; - //Specifies the maximum number of key-value pairs allowed. If the field has + // Specifies the maximum number of key-value pairs allowed. If the field has // more key-value pairs than specified, an error message is generated. // // ```proto @@ -4346,10 +4305,7 @@ message MapRules { expression: "uint(this.size()) > rules.max_pairs ? 'map must be at most %d entries'.format([rules.max_pairs]) : ''" }]; - //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. + // Specifies the rules to be applied to each key in the field. // // ```proto // message MyMap { @@ -4362,14 +4318,13 @@ message MapRules { // }]; // } // ``` + // + // Note that the `required` rule does not apply. Map keys cannot be unset. optional FieldRules keys = 4; - //Specifies the rules to be applied to the value of each key in the + // Specifies the rules to be applied to the value of each key in the // 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. + // `ignore` is specified. // // ```proto // message MyMap { @@ -4382,6 +4337,7 @@ message MapRules { // }]; // } // ``` + // Note that the `required` rule does not apply. Map values cannot be unset. optional FieldRules values = 5; // Extension fields in this range that have the (buf.validate.predefined)