diff --git a/CHANGELOG.md b/CHANGELOG.md index 2eb976101e..303b812582 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,7 @@ ## [Unreleased] -- No changes yet. +- Update `PROTOVALIDATE` lint rule to support field mask rules. ## [v1.62.1] - 2025-12-29 diff --git a/cmd/buf/buf_test.go b/cmd/buf/buf_test.go index d2c194a6ac..da53c74a00 100644 --- a/cmd/buf/buf_test.go +++ b/cmd/buf/buf_test.go @@ -3020,7 +3020,7 @@ testdata/check_plugins/current/proto/api/v1/service.proto:17:14:RPC request type testdata/check_plugins/current/proto/api/v1/service.proto:17:42:RPC response type "GetFooTestResponse" should be named "GetFooResponse" or "FooServiceTestGetFooResponse". testdata/check_plugins/current/proto/api/v1/service.proto:26:1:"ListFooResponse" is a pagination response without a page token field named "page_token" (buf-plugin-rpc-ext) testdata/check_plugins/current/proto/common/v1alpha1/messages.proto:16:5:field "common.v1alpha1.Four.FourTwo.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) -testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto:94:3:field "buf.validate.Rule.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) +testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto:95:3:field "buf.validate.Rule.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) `), "lint", filepath.Join("testdata", "check_plugins", "current"), @@ -3046,7 +3046,7 @@ testdata/check_plugins/current/proto/api/v1/service.proto:17:14:RPC request type testdata/check_plugins/current/proto/api/v1/service.proto:17:42:RPC response type "GetFooTestResponse" should be named "GetFooResponse" or "FooServiceTestGetFooResponse". testdata/check_plugins/current/proto/api/v1/service.proto:26:1:"ListFooResponse" is a pagination response without a page token field named "page_token" (buf-plugin-rpc-ext) testdata/check_plugins/current/proto/common/v1alpha1/messages.proto:16:5:field "common.v1alpha1.Four.FourTwo.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) -testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto:94:3:field "buf.validate.Rule.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) +testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto:95:3:field "buf.validate.Rule.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) `), "lint", filepath.Join("testdata", "check_plugins", "current"), @@ -3096,7 +3096,7 @@ testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto: filepath.FromSlash(` testdata/check_plugins/current/proto/api/v1/service.proto:11:1:Service name "api.v1.FooServiceMock" has banned suffix "Mock". (buf-plugin-suffix) testdata/check_plugins/current/proto/common/v1alpha1/messages.proto:16:5:field "common.v1alpha1.Four.FourTwo.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) -testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto:94:3:field "buf.validate.Rule.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) +testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto:95:3:field "buf.validate.Rule.id" does not have rule (buf.validate.field).string.tuuid set (buf-plugin-protovalidate-ext) `), "lint", filepath.Join("testdata", "check_plugins", "current"), diff --git a/cmd/buf/testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto b/cmd/buf/testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto index 519545ddfc..eefab5fbab 100644 --- a/cmd/buf/testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto +++ b/cmd/buf/testdata/check_plugins/current/vendor/protovalidate/buf/validate/validate.proto @@ -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"; @@ -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/). @@ -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/). @@ -313,6 +349,7 @@ message FieldRules { // Well-Known Field Types AnyRules any = 20; DurationRules duration = 21; + FieldMaskRules field_mask = 28; TimestampRules timestamp = 22; } @@ -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. @@ -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"]; @@ -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"}]; @@ -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 @@ -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. diff --git a/cmd/buf/testdata/check_plugins/previous/vendor/protovalidate/buf/validate/validate.proto b/cmd/buf/testdata/check_plugins/previous/vendor/protovalidate/buf/validate/validate.proto index 519545ddfc..eefab5fbab 100644 --- a/cmd/buf/testdata/check_plugins/previous/vendor/protovalidate/buf/validate/validate.proto +++ b/cmd/buf/testdata/check_plugins/previous/vendor/protovalidate/buf/validate/validate.proto @@ -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"; @@ -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/). @@ -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/). @@ -313,6 +349,7 @@ message FieldRules { // Well-Known Field Types AnyRules any = 20; DurationRules duration = 21; + FieldMaskRules field_mask = 28; TimestampRules timestamp = 22; } @@ -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. @@ -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"]; @@ -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"}]; @@ -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 @@ -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. diff --git a/make/buf/all.mk b/make/buf/all.mk index 08286fe1f3..63f1c7ecc0 100644 --- a/make/buf/all.mk +++ b/make/buf/all.mk @@ -46,7 +46,7 @@ LICENSE_HEADER_YEAR_RANGE := 2020-2025 LICENSE_HEADER_IGNORES := \/testdata enterprise BANDEPS_CONFIG := etc/bandeps/bandeps.yaml BUFPRIVATEUSAGE_PKGS := ./private/... -PROTOVALIDATE_VERSION := v1.0.0 +PROTOVALIDATE_VERSION := v1.1.0 # Comment out to use released buf #BUF_GO_INSTALL_PATH := ./cmd/buf diff --git a/private/bufpkg/bufcheck/bufcheckserver/internal/buflintvalidate/field.go b/private/bufpkg/bufcheck/bufcheckserver/internal/buflintvalidate/field.go index 2c9aa7c91a..514274ecb9 100644 --- a/private/bufpkg/bufcheck/bufcheckserver/internal/buflintvalidate/field.go +++ b/private/bufpkg/bufcheck/bufcheckserver/internal/buflintvalidate/field.go @@ -18,6 +18,7 @@ import ( "errors" "fmt" "regexp" + "slices" "strings" "unicode/utf8" @@ -32,6 +33,7 @@ import ( "google.golang.org/protobuf/types/dynamicpb" "google.golang.org/protobuf/types/known/anypb" "google.golang.org/protobuf/types/known/durationpb" + "google.golang.org/protobuf/types/known/fieldmaskpb" "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -67,6 +69,7 @@ const ( requiredFieldNumber = 25 ignoreEmptyFieldNumber = 26 ignoreFieldNumber = 27 + fieldMaskRulesFieldNumber = 28 // https://buf.build/bufbuild/protovalidate/docs/v0.5.1:buf.validate#buf.validate.StringRules minLenFieldNumberInStringRules = 2 maxLenFieldNumberInStringRules = 3 @@ -98,6 +101,8 @@ const ( ltNowFieldNumberInTimestampRules = 7 gtNowFieldNumberInTimestampRules = 8 withInFieldNumberInTimestampRules = 9 + // https://buf.build/bufbuild/protovalidate/docs/v1.1.0:buf.validate#buf.validate.FieldMaskRules + inFieldMaskRules = 2 exampleName = "example" ) @@ -134,6 +139,7 @@ var ( anyRulesFieldNumber: string((&anypb.Any{}).ProtoReflect().Descriptor().FullName()), durationRulesFieldNumber: string((&durationpb.Duration{}).ProtoReflect().Descriptor().FullName()), timestampRulesFieldNumber: string((×tamppb.Timestamp{}).ProtoReflect().Descriptor().FullName()), + fieldMaskRulesFieldNumber: string((&fieldmaskpb.FieldMask{}).ProtoReflect().Descriptor().FullName()), } wrapperTypeNames = map[string]struct{}{ string((&wrapperspb.FloatValue{}).ProtoReflect().Descriptor().FullName()): {}, @@ -290,6 +296,8 @@ func checkRulesForField( return checkDurationRules(adder, fieldRules.GetDuration()) case timestampRulesFieldNumber: return checkTimestampRules(adder, fieldRules.GetTimestamp()) + case fieldMaskRulesFieldNumber: + checkFieldMaskRules(adder, fieldRules.GetFieldMask()) } return nil } @@ -738,6 +746,22 @@ func checkTimestampRules(adder *adder, timestampRules *validate.TimestampRules) return nil } +func checkFieldMaskRules(adder *adder, fieldMaskRules *validate.FieldMaskRules) { + checkConst(adder, fieldMaskRules, fieldMaskRulesFieldNumber) + if len(fieldMaskRules.In) > 0 && len(fieldMaskRules.NotIn) > 0 { + for _, in := range fieldMaskRules.In { + if slices.Contains(fieldMaskRules.NotIn, in) { + adder.addForPathf( + []int32{fieldMaskRulesFieldNumber, inFieldMaskRules}, + "Field %q has path %q in both in and not_in rules.", + adder.fieldName(), + in, + ) + } + } + } +} + func checkExampleValues( adder *adder, pathToExampleValues []int32, diff --git a/private/bufpkg/bufcheck/lint_test.go b/private/bufpkg/bufcheck/lint_test.go index ed1443e97b..8d0f20244b 100644 --- a/private/bufpkg/bufcheck/lint_test.go +++ b/private/bufpkg/bufcheck/lint_test.go @@ -659,6 +659,9 @@ func TestRunProtovalidate(t *testing.T) { bufanalysistesting.NewFileAnnotation(t, "field.proto", 18, 5, 18, 41, "PROTOVALIDATE"), bufanalysistesting.NewFileAnnotation(t, "field.proto", 19, 5, 19, 55, "PROTOVALIDATE"), bufanalysistesting.NewFileAnnotation(t, "field.proto", 23, 52, 23, 102, "PROTOVALIDATE"), + bufanalysistesting.NewFileAnnotation(t, "field_mask.proto", 16, 45, 16, 88, "PROTOVALIDATE"), + bufanalysistesting.NewFileAnnotation(t, "field_mask.proto", 20, 5, 22, 6, "PROTOVALIDATE"), + bufanalysistesting.NewFileAnnotation(t, "field_mask.proto", 31, 5, 31, 45, "PROTOVALIDATE"), bufanalysistesting.NewFileAnnotation(t, "map.proto", 24, 38, 24, 76, "PROTOVALIDATE"), bufanalysistesting.NewFileAnnotation(t, "map.proto", 27, 5, 27, 43, "PROTOVALIDATE"), bufanalysistesting.NewFileAnnotation(t, "map.proto", 29, 5, 29, 43, "PROTOVALIDATE"), diff --git a/private/bufpkg/bufcheck/testdata/lint/protovalidate/proto/field_mask.proto b/private/bufpkg/bufcheck/testdata/lint/protovalidate/proto/field_mask.proto new file mode 100644 index 0000000000..5ec1beb23a --- /dev/null +++ b/private/bufpkg/bufcheck/testdata/lint/protovalidate/proto/field_mask.proto @@ -0,0 +1,35 @@ +syntax = "proto3"; + +package example.v1; + +import "buf/validate/validate.proto"; +import "google/protobuf/field_mask.proto"; + +message FieldMaskTest { + // valid + google.protobuf.FieldMask const_field_mask = 1 [(buf.validate.field).field_mask.const = { + paths: ["a"] + }]; + google.protobuf.FieldMask in_field_mask = 2 [(buf.validate.field).field_mask.in = "some_field"]; + google.protobuf.FieldMask not_in_field_mask = 3 [(buf.validate.field).field_mask.not_in = "some_other_field"]; + // invalid + google.protobuf.FieldMask not_string = 4 [(buf.validate.field).string.const = "field"]; + // const should be the only field if defined + google.protobuf.FieldMask in_and_const = 5 [ + (buf.validate.field).field_mask.in = "a", + (buf.validate.field).field_mask.const = { + paths: ["b"] + }]; + // valid + google.protobuf.FieldMask no_conflict = 6 [ + (buf.validate.field).field_mask.in = "a", + (buf.validate.field).field_mask.not_in = "b", + (buf.validate.field).field_mask.not_in = "c" + ]; + // conflict between in and not_in + google.protobuf.FieldMask conflict = 7 [ + (buf.validate.field).field_mask.in = "a", + (buf.validate.field).field_mask.not_in = "b", + (buf.validate.field).field_mask.not_in = "a" + ]; +} diff --git a/private/bufpkg/bufcheck/testdata/lint/protovalidate/vendor/protovalidate/buf/validate/validate.proto b/private/bufpkg/bufcheck/testdata/lint/protovalidate/vendor/protovalidate/buf/validate/validate.proto index 519545ddfc..eefab5fbab 100644 --- a/private/bufpkg/bufcheck/testdata/lint/protovalidate/vendor/protovalidate/buf/validate/validate.proto +++ b/private/bufpkg/bufcheck/testdata/lint/protovalidate/vendor/protovalidate/buf/validate/validate.proto @@ -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"; @@ -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/). @@ -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/). @@ -313,6 +349,7 @@ message FieldRules { // Well-Known Field Types AnyRules any = 20; DurationRules duration = 21; + FieldMaskRules field_mask = 28; TimestampRules timestamp = 22; } @@ -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. @@ -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"]; @@ -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"}]; @@ -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 @@ -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. diff --git a/private/bufpkg/bufcheck/testdata/lint/protovalidate_predefined/vendor/protovalidate/buf/validate/validate.proto b/private/bufpkg/bufcheck/testdata/lint/protovalidate_predefined/vendor/protovalidate/buf/validate/validate.proto index 519545ddfc..eefab5fbab 100644 --- a/private/bufpkg/bufcheck/testdata/lint/protovalidate_predefined/vendor/protovalidate/buf/validate/validate.proto +++ b/private/bufpkg/bufcheck/testdata/lint/protovalidate_predefined/vendor/protovalidate/buf/validate/validate.proto @@ -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"; @@ -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/). @@ -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/). @@ -313,6 +349,7 @@ message FieldRules { // Well-Known Field Types AnyRules any = 20; DurationRules duration = 21; + FieldMaskRules field_mask = 28; TimestampRules timestamp = 22; } @@ -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. @@ -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"]; @@ -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"}]; @@ -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 @@ -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.