From 46552150131f3c7bf698d81b8e5f6cb4862d005c Mon Sep 17 00:00:00 2001 From: Oliver Sun <73540835+oliversun9@users.noreply.github.com> Date: Tue, 19 Sep 2023 15:20:24 -0400 Subject: [PATCH] Add more examples (#93) `option_*` cover most features from [the standard options](https://buf.build/bufbuild/protovalidate/docs/main:buf.validate). `cel_*` cover most features from [CEL's feature list](https://github.com/google/cel-spec/blob/master/doc/langdef.md#list-of-standard-definitions). Exception: - From timestamp's `getFooFromBar` family, including `getDate`, `getDayOfMonth`, `getDayOfYear`, `getFullYear`, `getHours`, `getMilliseconds`, `getMinutes`, `getMonth` and `getSeconds`, only `getDayOrWeek` is covered, but there is a comment mentioning all the rest and a link to this feature list. `cel_*` also cover [custom CEL functions and overloads](https://github.com/bufbuild/protovalidate/blob/main/docs/cel.md#custom-variables-functions-and-overloads) --- examples/BUILD.bazel | 70 +++++++++++-- examples/cel_assert_value_is_in_a_list.proto | 42 ++++++++ examples/cel_bytes_concatentation.proto | 30 ++++++ examples/cel_bytes_contains.proto | 32 ++++++ .../cel_bytes_starts_with_ends_with.proto | 37 +++++++ examples/cel_duration_arithmetic.proto | 37 +++++++ examples/cel_field_mask.proto | 54 ++++++++++ examples/cel_field_presence_nested.proto | 45 +++++++++ examples/cel_infinity.proto | 35 +++++++ examples/cel_list_concatenation.proto | 32 ++++++ .../{cel_map_key.proto => cel_map_all.proto} | 0 examples/cel_map_exists.proto | 57 +++++++++++ examples/cel_map_exists_one.proto | 37 +++++++ examples/cel_map_size.proto | 30 ++++++ examples/cel_number_arithmetic.proto | 31 ++++++ ...cel_repeated_field_filter_and_count.proto} | 0 examples/cel_repeated_field_unique.proto | 29 ++++++ examples/cel_string_concatentation.proto | 35 +++++++ examples/cel_string_is_email.proto | 31 ++++++ examples/cel_string_is_hostname.proto | 27 +++++ examples/cel_string_is_ip.proto | 38 +++++++ examples/cel_string_is_uri.proto | 35 +++++++ examples/cel_timestamp_comparsion.proto | 34 +++++++ ...roto => cel_timestamp_get_attribute.proto} | 8 +- examples/cel_timestamp_subtraction.proto | 8 +- examples/cel_type_conversion.proto | 32 ++++++ examples/cel_value.proto | 37 +++++++ examples/cel_wrapper_type.proto | 99 +++++++++++++++++++ examples/option_any_type_allow_list.proto | 34 +++++++ ...y.proto => option_any_type_ban_list.proto} | 8 +- examples/option_bytes_ban_values.proto | 40 ++++++++ examples/option_bytes_contains.proto | 24 +++++ examples/option_bytes_equal.proto | 30 ++++++ ...ion_bytes.proto => option_bytes_len.proto} | 5 +- examples/option_bytes_pattern.proto | 31 ++++++ examples/option_bytes_prefix_suffix.proto | 29 ++++++ examples/option_duration_allow_values.proto | 31 ++++++ .../option_duration_disallow_values.proto | 37 +++++++ examples/option_duration_equal.proto | 30 ++++++ ...tion.proto => option_duration_range.proto} | 0 examples/option_enum_allow_values.proto | 51 ++++++++++ examples/option_enum_disallow_values.proto | 47 +++++++++ examples/option_field_ignore_empty.proto | 36 +++++++ ...ence.proto => option_field_presence.proto} | 0 examples/option_field_skip_validation.proto | 41 ++++++++ .../option_message_disable_validation.proto | 33 +++++++ examples/option_number_allow_values.proto | 42 ++++++++ examples/option_number_disallow_values.proto | 35 +++++++ examples/option_number_equal.proto | 28 ++++++ .../option_number_finite_and_infinite.proto | 42 ++++++++ ...number.proto => option_number_range.proto} | 2 +- examples/option_string.proto | 43 -------- examples/option_string_allow_values.proto | 28 ++++++ examples/option_string_ban_values.proto | 35 +++++++ examples/option_string_contains.proto | 29 ++++++ examples/option_string_equal.proto | 29 ++++++ examples/option_string_is_http_header.proto | 41 ++++++++ examples/option_string_len.proto | 41 ++++++++ examples/option_string_match_pattern.proto | 63 ++++++++++++ examples/option_string_prefix_suffix.proto | 35 +++++++ examples/option_timestamp_range.proto | 37 +++++++ .../option_timestamp_relative_to_now.proto | 34 +++++++ 62 files changed, 1991 insertions(+), 62 deletions(-) create mode 100644 examples/cel_assert_value_is_in_a_list.proto create mode 100644 examples/cel_bytes_concatentation.proto create mode 100644 examples/cel_bytes_contains.proto create mode 100644 examples/cel_bytes_starts_with_ends_with.proto create mode 100644 examples/cel_duration_arithmetic.proto create mode 100644 examples/cel_field_mask.proto create mode 100644 examples/cel_field_presence_nested.proto create mode 100644 examples/cel_infinity.proto create mode 100644 examples/cel_list_concatenation.proto rename examples/{cel_map_key.proto => cel_map_all.proto} (100%) create mode 100644 examples/cel_map_exists.proto create mode 100644 examples/cel_map_exists_one.proto create mode 100644 examples/cel_map_size.proto create mode 100644 examples/cel_number_arithmetic.proto rename examples/{cel_repeated_field_filter_and_size.proto => cel_repeated_field_filter_and_count.proto} (100%) create mode 100644 examples/cel_repeated_field_unique.proto create mode 100644 examples/cel_string_concatentation.proto create mode 100644 examples/cel_string_is_email.proto create mode 100644 examples/cel_string_is_hostname.proto create mode 100644 examples/cel_string_is_ip.proto create mode 100644 examples/cel_string_is_uri.proto create mode 100644 examples/cel_timestamp_comparsion.proto rename examples/{cel_timestamp_to_day_of_week.proto => cel_timestamp_get_attribute.proto} (80%) create mode 100644 examples/cel_type_conversion.proto create mode 100644 examples/cel_value.proto create mode 100644 examples/cel_wrapper_type.proto create mode 100644 examples/option_any_type_allow_list.proto rename examples/{option_wkt_any.proto => option_any_type_ban_list.proto} (74%) create mode 100644 examples/option_bytes_ban_values.proto create mode 100644 examples/option_bytes_contains.proto create mode 100644 examples/option_bytes_equal.proto rename examples/{option_bytes.proto => option_bytes_len.proto} (83%) create mode 100644 examples/option_bytes_pattern.proto create mode 100644 examples/option_bytes_prefix_suffix.proto create mode 100644 examples/option_duration_allow_values.proto create mode 100644 examples/option_duration_disallow_values.proto create mode 100644 examples/option_duration_equal.proto rename examples/{option_wkt_duration.proto => option_duration_range.proto} (100%) create mode 100644 examples/option_enum_allow_values.proto create mode 100644 examples/option_enum_disallow_values.proto create mode 100644 examples/option_field_ignore_empty.proto rename examples/{option_require_field_presence.proto => option_field_presence.proto} (100%) create mode 100644 examples/option_field_skip_validation.proto create mode 100644 examples/option_message_disable_validation.proto create mode 100644 examples/option_number_allow_values.proto create mode 100644 examples/option_number_disallow_values.proto create mode 100644 examples/option_number_equal.proto create mode 100644 examples/option_number_finite_and_infinite.proto rename examples/{option_number.proto => option_number_range.proto} (97%) delete mode 100644 examples/option_string.proto create mode 100644 examples/option_string_allow_values.proto create mode 100644 examples/option_string_ban_values.proto create mode 100644 examples/option_string_contains.proto create mode 100644 examples/option_string_equal.proto create mode 100644 examples/option_string_is_http_header.proto create mode 100644 examples/option_string_len.proto create mode 100644 examples/option_string_match_pattern.proto create mode 100644 examples/option_string_prefix_suffix.proto create mode 100644 examples/option_timestamp_range.proto create mode 100644 examples/option_timestamp_relative_to_now.proto diff --git a/examples/BUILD.bazel b/examples/BUILD.bazel index 77a49b26..2f618b26 100644 --- a/examples/BUILD.bazel +++ b/examples/BUILD.bazel @@ -18,32 +18,82 @@ load("@rules_buf//buf:defs.bzl", "buf_lint_test") proto_library( name = "examples_proto", srcs = [ + "cel_assert_value_is_in_a_list.proto", + "cel_bytes_concatentation.proto", + "cel_bytes_contains.proto", + "cel_bytes_starts_with_ends_with.proto", "cel_conditional_operator.proto", + "cel_duration_arithmetic.proto", "cel_duration_from_string.proto", "cel_enum_comparision.proto", "cel_field_access.proto", + "cel_field_mask.proto", "cel_field_presence.proto", + "cel_field_presence_nested.proto", "cel_field_selection.proto", - "cel_map_key.proto", + "cel_infinity.proto", + "cel_list_concatenation.proto", + "cel_map_all.proto", + "cel_map_exists.proto", + "cel_map_exists_one.proto", + "cel_map_size.proto", + "cel_number_arithmetic.proto", "cel_repeated_field_all.proto", "cel_repeated_field_exists_one.proto", - "cel_repeated_field_filter_and_size.proto", + "cel_repeated_field_filter_and_count.proto", + "cel_repeated_field_unique.proto", + "cel_string_concatentation.proto", "cel_string_contains.proto", + "cel_string_is_email.proto", + "cel_string_is_hostname.proto", + "cel_string_is_ip.proto", + "cel_string_is_uri.proto", "cel_string_match_pattern.proto", "cel_string_starts_with_ends_with.proto", + "cel_timestamp_comparsion.proto", + "cel_timestamp_get_attribute.proto", "cel_timestamp_plus_duration.proto", "cel_timestamp_subtraction.proto", - "cel_timestamp_to_day_of_week.proto", + "cel_type_conversion.proto", + "cel_value.proto", + "cel_wrapper_type.proto", + "option_any_type_allow_list.proto", + "option_any_type_ban_list.proto", "option_bool.proto", - "option_bytes.proto", + "option_bytes_ban_values.proto", + "option_bytes_contains.proto", + "option_bytes_equal.proto", + "option_bytes_len.proto", + "option_bytes_pattern.proto", + "option_bytes_prefix_suffix.proto", + "option_duration_allow_values.proto", + "option_duration_disallow_values.proto", + "option_duration_equal.proto", + "option_duration_range.proto", + "option_enum_allow_values.proto", + "option_enum_disallow_values.proto", + "option_field_ignore_empty.proto", + "option_field_presence.proto", + "option_field_skip_validation.proto", "option_map.proto", - "option_number.proto", + "option_message_disable_validation.proto", + "option_number_allow_values.proto", + "option_number_disallow_values.proto", + "option_number_equal.proto", + "option_number_finite_and_infinite.proto", + "option_number_range.proto", "option_oneof.proto", "option_repeated.proto", - "option_require_field_presence.proto", - "option_string.proto", - "option_wkt_any.proto", - "option_wkt_duration.proto", + "option_string_allow_values.proto", + "option_string_ban_values.proto", + "option_string_contains.proto", + "option_string_equal.proto", + "option_string_is_http_header.proto", + "option_string_len.proto", + "option_string_match_pattern.proto", + "option_string_prefix_suffix.proto", + "option_timestamp_range.proto", + "option_timestamp_relative_to_now.proto", ], strip_import_prefix = "/examples", visibility = ["//visibility:public"], @@ -51,6 +101,8 @@ proto_library( "//proto/protovalidate/buf/validate:validate_proto", "@com_google_protobuf//:any_proto", "@com_google_protobuf//:duration_proto", + "@com_google_protobuf//:field_mask_proto", + "@com_google_protobuf//:struct_proto", "@com_google_protobuf//:timestamp_proto", "@com_google_protobuf//:wrappers_proto", "@go_googleapis//google/type:postaladdress_proto", diff --git a/examples/cel_assert_value_is_in_a_list.proto b/examples/cel_assert_value_is_in_a_list.proto new file mode 100644 index 00000000..bb7f4eaf --- /dev/null +++ b/examples/cel_assert_value_is_in_a_list.proto @@ -0,0 +1,42 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +service DivisionService { + // IsDivisible solves whether one number is divisible by another. + rpc IsDivisible(IsDivisibleRequest) returns (IsDivisibleResponse); +} + +message IsDivisibleRequest { + // dividend is the dividend in the division. + int32 dividend = 1; + // divisor is the divisor in the division. + int32 divisor = 2 [(buf.validate.field).cel = { + id: "divisor_must_be_a_small_prime", + message: "the divisor must be one of 2, 3 and 5", + // `in` evaluates list membership. The expression evaluates to + // true when the value is in the list. + // Validates that divisor must be one of 2, 3 and 5. + expression: "this in [2, 3, 5]", + // Similarly, you can assert that it's not in a list with `!(this in [2, 3, 5])`. + }]; +} + +message IsDivisibleResponse { + // is_divisible is the result. + bool is_divisible = 1; +} diff --git a/examples/cel_bytes_concatentation.proto b/examples/cel_bytes_concatentation.proto new file mode 100644 index 00000000..717a25f3 --- /dev/null +++ b/examples/cel_bytes_concatentation.proto @@ -0,0 +1,30 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +message CompactDocument { + option (buf.validate.message).cel = { + id: "header_footer_size_limit", + message: "header and footer should be less than 500 bytes in total", + // the `+` operator concatenates two `bytes` together and `size` returns + // the length of a `bytes`. + // This validates that the combined size of header and footer should be < 500. + expression: "size(this.header + this.footer) < 500" + }; + bytes header = 1; + bytes footer = 2; +} diff --git a/examples/cel_bytes_contains.proto b/examples/cel_bytes_contains.proto new file mode 100644 index 00000000..e734910b --- /dev/null +++ b/examples/cel_bytes_contains.proto @@ -0,0 +1,32 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +// Application is an application. +message Application { + // binary is the application's binary + bytes binary = 1 [(buf.validate.field).cel = { + id: "without_malicious_code", + message: "binary should not contain malicious code", + // `contains` returns if the receiver `bytes` contains the argument `bytes`. + // This validates that the application binary should not contain bytes for + // `'malicious code'`. + // Note that when checking whether bytes for a string is contained, `bytes()` + // must be called to convert the string into bytes. + expression: "!this.contains(bytes('malicious code'))" + }]; +} diff --git a/examples/cel_bytes_starts_with_ends_with.proto b/examples/cel_bytes_starts_with_ends_with.proto new file mode 100644 index 00000000..1e008f40 --- /dev/null +++ b/examples/cel_bytes_starts_with_ends_with.proto @@ -0,0 +1,37 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +message ScriptFile { + bytes content = 1 [(buf.validate.field).cel = { + id: "script_start_with_shabang_end_with_line_feed", + expression: + // this is a ternary expression in the form of a ? b : c, if the the + // script file doesn't start with '#!', the expression evaluates to the + // first error message, which fails the validation. Otherwise it + // evaluates to the next expression. + "!this.startsWith(bytes('#!')) ? 'must start with #!'" + // If this file does not end with the new line character, it evaluates + // to the second error message. + // Note: the `'\x0A'` is byte string for the new line character, but + // we have two backslashes below. This is because we need to escape the + // backslash. + ": !this.endsWith(bytes('\\x0A')) ? 'must end with a new line'" + // If the file passes the two checks above, there is no error. + ": ''" + }]; +} diff --git a/examples/cel_duration_arithmetic.proto b/examples/cel_duration_arithmetic.proto new file mode 100644 index 00000000..1e0d0920 --- /dev/null +++ b/examples/cel_duration_arithmetic.proto @@ -0,0 +1,37 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; +import "google/protobuf/duration.proto"; + +message IndirectFlight { + option (buf.validate.message).cel = { + id: "totol_lenght_limit", + message: "the entire trip should be less than 48 hours", + expression: + // This validates that the total duration of an indirect flight should be + // less than 48 hours. + // Adding two duration yields a duration. + // Note that if the CEL expression is too long, you can break it down into + // multiple lines like so: + "this.first_flight_duration + this.second_flight_duration" + // Durations can be compared with `<`, `<=`, `>=`, `>`, `==` and `!=`. + "+ this.layover_duration < duration('48h')" + }; + google.protobuf.Duration first_flight_duration = 1; + google.protobuf.Duration layover_duration = 2; + google.protobuf.Duration second_flight_duration = 3; +} diff --git a/examples/cel_field_mask.proto b/examples/cel_field_mask.proto new file mode 100644 index 00000000..7e0d4f76 --- /dev/null +++ b/examples/cel_field_mask.proto @@ -0,0 +1,54 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; +import "google/protobuf/duration.proto"; +import "google/protobuf/field_mask.proto"; +import "google/protobuf/timestamp.proto"; + +service SongService { + rpc UpdateSong(UpdateSongRequest) returns (UpdateSongResponse); +} + +message UpdateSongRequest { + // song is the updated song. + Song song = 1; + // field_mask masks the fields to update. + google.protobuf.FieldMask field_mask = 2 [(buf.validate.field).cel = { + id: "valid_field_mask", + message: "a field mask path must be one of name, duration and artist.name", + // `some_list.all()` validates that a predicate is true for all elements + // from that list. + // In this case, `this.paths` is the field paths from. + expression: "this.paths.all(path, path in ['name', 'duration', 'artist.name'])", + }]; +} + +message Song { + string name = 1; + fixed64 id = 2; + google.protobuf.Timestamp create_time = 3; + google.protobuf.Duration duration = 4; + Artist artist = 5; +} + +message Artist { + string name = 1; + // other fields: + // ... +} + +message UpdateSongResponse {} diff --git a/examples/cel_field_presence_nested.proto b/examples/cel_field_presence_nested.proto new file mode 100644 index 00000000..39c4e2a4 --- /dev/null +++ b/examples/cel_field_presence_nested.proto @@ -0,0 +1,45 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +service ContactService { + // CreateContact creates a contact. + rpc CreateContact(CreateContactRequest) returns (CreateContactResponse); +} + +message CreateContactRequest { + option (buf.validate.message).cel = { + id: "create_contact_with_first_name", + message: "the contact must have first name", + // This expression validates that all of `this.contact`, `this.contact.full_name` + // and `this.contact.full_name.first_name` are set. + expression: "has(this.contact.full_name.first_name)" + }; + // contact is the contact to create. + Contact contact = 1; +} + +message Contact { + FullName full_name = 1; +} + +message FullName { + string first_name = 1; + string last_name = 2; +} + +message CreateContactResponse {} diff --git a/examples/cel_infinity.proto b/examples/cel_infinity.proto new file mode 100644 index 00000000..5a50420c --- /dev/null +++ b/examples/cel_infinity.proto @@ -0,0 +1,35 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +service BalanceService { + // SetBalance sets balance. + rpc SetBalance(SetBalanceRequest) returns (SetBalanceResponse); +} + +message SetBalanceRequest { + double new_balance = 1 [(buf.validate.field).cel = { + id: "finite_balance", + message: "balance should be finite", + // `isInf()` works on float and double, and returns true if the value is + // infinity. + // This validates that the new balance is finite. + expression: "!this.isInf()" + }]; +} + +message SetBalanceResponse {} diff --git a/examples/cel_list_concatenation.proto b/examples/cel_list_concatenation.proto new file mode 100644 index 00000000..dd075731 --- /dev/null +++ b/examples/cel_list_concatenation.proto @@ -0,0 +1,32 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +message BeverageMenu { + repeated string hot_beverages = 1; + repeated string cold_beverages = 2; + string daily_special = 3; + + option (buf.validate.message).cel = { + id: "daily_special_either_hot_or_cold", + message: "the daily special must be in either the hot menu or the cold menu", + // The `+` is loaded for lists (repeated fields) and it concatenates two lists. + // In this case `this.hot_beverages` + `this.cold_beverages` evaluates to a list + // containing beverages from both lists. + expression: "this.daily_special in this.hot_beverages + this.cold_beverages" + }; +} diff --git a/examples/cel_map_key.proto b/examples/cel_map_all.proto similarity index 100% rename from examples/cel_map_key.proto rename to examples/cel_map_all.proto diff --git a/examples/cel_map_exists.proto b/examples/cel_map_exists.proto new file mode 100644 index 00000000..6e99ce46 --- /dev/null +++ b/examples/cel_map_exists.proto @@ -0,0 +1,57 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +// CustomerReviews is customers' reviews for a product. +message CustomerReviews { + // customer_id_to_review maps a customer id to a review. + map customer_id_to_review = 1; + // highest_rating is the highest rating this product receives. + float highest_rating = 2; + // This validates that there is at least one review from customer_id_to_reivew + // such that its rating is the same as highest_rating. + option (buf.validate.message).cel = { + id: "highest_rating_exists", + message: "highest_rating must be the highest from reviews", + expression: + // `some_map.exists` validates that a predicate (condition) is true for some + // key in this map. To validate that a predicate is true for some _value_ in + // a map, access the value with `some_map[key]` in the predicate to validate + // the value. + "this.customer_id_to_review.exists(" + " id," + " this.customer_id_to_review[id].rating == this.highest_rating" + ")" + }; +} + +message ProductReview { + // rating is how much the review rates a product. + float rating = 1; + // review is the detailed review for a product. + string review = 2; +} diff --git a/examples/cel_map_exists_one.proto b/examples/cel_map_exists_one.proto new file mode 100644 index 00000000..73cbf42b --- /dev/null +++ b/examples/cel_map_exists_one.proto @@ -0,0 +1,37 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +// TeamRoles has role information for each player. +message TeamRoles { + // name_to_role maps from a team member's name to role. + map name_to_role = 1; + option (buf.validate.message).cel = { + id: "exactly_one_captain", + message: "there must be exactly one captain", + // `exists_one` validates that exactly one key in the map satifies the predicate. + // In this case, it validates that exactly one player in the team has the + // captain role. + expression: "this.name_to_role.exists_one(name, this.name_to_role[name] == 1)" + }; +} + +enum TeamRole { + TEAM_ROLE_UNSPECIFIED = 0; + TEAM_ROLE_CAPTAIN = 1; + TEAM_ROLE_PLAYER = 2; +} diff --git a/examples/cel_map_size.proto b/examples/cel_map_size.proto new file mode 100644 index 00000000..9c543f93 --- /dev/null +++ b/examples/cel_map_size.proto @@ -0,0 +1,30 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +// PenInventory is the pen inventory. +message PenInventory { + // color_to_count maps from a color id to the amount of pens of the color. + map color_to_count = 1; + option (buf.validate.message).cel = { + id: "at_most_30_colors", + message: "there must not be more than 30 colors", + // `size(some_map)` returns the number of keys in this map. + // In this case, it validates that the map has at most 30 keys. + expression: "size(this.color_to_count) <= 30" + }; +} diff --git a/examples/cel_number_arithmetic.proto b/examples/cel_number_arithmetic.proto new file mode 100644 index 00000000..9930fa60 --- /dev/null +++ b/examples/cel_number_arithmetic.proto @@ -0,0 +1,31 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +message FiveDigitPrimeLookalike { + int32 value = 1 [(buf.validate.field).cel = { + id: "prime_lookalike", + message: "value must have 5 digits and look like a prime", + // Arithmetic operators include `+`, `-`, `*`, `/`, `%`. + expression: + "this % 2 != 0" + "&& this % 3 != 0" + "&& this % 5 != 0" + "&& (this - 10000) * (this - 99999) <= 0" + "&& -this != 77777", + }]; +} diff --git a/examples/cel_repeated_field_filter_and_size.proto b/examples/cel_repeated_field_filter_and_count.proto similarity index 100% rename from examples/cel_repeated_field_filter_and_size.proto rename to examples/cel_repeated_field_filter_and_count.proto diff --git a/examples/cel_repeated_field_unique.proto b/examples/cel_repeated_field_unique.proto new file mode 100644 index 00000000..8b42d1e4 --- /dev/null +++ b/examples/cel_repeated_field_unique.proto @@ -0,0 +1,29 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +message Book { + option (buf.validate.message).cel = { + id: "book_unique_authors", + message: "a name cannot appear twice in authors", + // `unique()` evaluates to whether each element in a list is unique. + // In this case, the expression validates that names in the list of authors + // should be unique. + expression: "this.authors.unique()", + }; + repeated string authors = 1; +} diff --git a/examples/cel_string_concatentation.proto b/examples/cel_string_concatentation.proto new file mode 100644 index 00000000..9b1d204e --- /dev/null +++ b/examples/cel_string_concatentation.proto @@ -0,0 +1,35 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +service NewsLetterService { + rpc JoinNewsLetter(JoinNewsLetterRequest) returns (JoinNewsLetterResponse); +} + +message JoinNewsLetterRequest { + string email = 1 [(buf.validate.field).cel = { + id: "join_news_letter_request_valid_email", + expression: + "this.isEmail() ? ''" + // The `+` operator is overloaded for strings and concatenates two strings. + // In this case, it is used to construct a more meaningful error message. + // An error message will look like: "xyz" is not a valid email. + ": '\"' + this + '\" is not a valid email'" + }]; +} + +message JoinNewsLetterResponse {} diff --git a/examples/cel_string_is_email.proto b/examples/cel_string_is_email.proto new file mode 100644 index 00000000..192aa672 --- /dev/null +++ b/examples/cel_string_is_email.proto @@ -0,0 +1,31 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +service MailingListService { + rpc AddEmailToMailingList(AddEmailToMailingListRequest) returns (AddEmailToMailingListResponse); +} + +message AddEmailToMailingListRequest { + string email = 1 [(buf.validate.field).cel = { + id: "valid_email", + message: "email must be a valid email", + expression: "this.isEmail()" + }]; +} + +message AddEmailToMailingListResponse {} diff --git a/examples/cel_string_is_hostname.proto b/examples/cel_string_is_hostname.proto new file mode 100644 index 00000000..054bb80e --- /dev/null +++ b/examples/cel_string_is_hostname.proto @@ -0,0 +1,27 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +message DeviceInfo { + // hostname is the device's hostname on the network. + string hostname = 1 [(buf.validate.field).cel = { + id: "device_info_valid_hostname", + message: "hostname must be valid", + // this validates that field `hostname` is a valid hostname. + expression: "this.isHostname()" + }]; +} diff --git a/examples/cel_string_is_ip.proto b/examples/cel_string_is_ip.proto new file mode 100644 index 00000000..897a08ae --- /dev/null +++ b/examples/cel_string_is_ip.proto @@ -0,0 +1,38 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +service LocationService { + rpc LocationForIp(LocationForIpRequest) returns (LocationForIpResponse); +} + +message LocationForIpRequest { + string ip_address = 1 [(buf.validate.field).cel = { + id: "valid_address", + message: ".", + // `some_string.isIp()` returns whether the string is a valid ip address. + // `isIp(4)` returns whehter a strign is an ipv4 address. + // `isIp(6)` returns whehter a strign is an ipv6 address. + // In this case, it validates that field `ip_address` must be a valid ip + // adress, either ipv4 or ipv6. + expression: "this.isIp()" + }]; +} + +message LocationForIpResponse { + // ... +} diff --git a/examples/cel_string_is_uri.proto b/examples/cel_string_is_uri.proto new file mode 100644 index 00000000..c0b29b20 --- /dev/null +++ b/examples/cel_string_is_uri.proto @@ -0,0 +1,35 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; + +service ResourceService { + rpc UploadResource(UploadResourceRequest) returns (UploadResourceResponse); +} + +message UploadResourceRequest { + string uri = 1 [(buf.validate.field).cel = { + id: "valid_uri", + message: "uri must be a valid URI", + // `isUri` validates that a string is an absolute URI. + // This expression validates that the uri field is an absolute URI. + // Note: to allow relative URI, use `isUriRef`. + expression: "this.isUri()" + }]; + bytes data = 2; +} + +message UploadResourceResponse {} diff --git a/examples/cel_timestamp_comparsion.proto b/examples/cel_timestamp_comparsion.proto new file mode 100644 index 00000000..4a6d8308 --- /dev/null +++ b/examples/cel_timestamp_comparsion.proto @@ -0,0 +1,34 @@ +// Copyright 2023 Buf Technologies, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +syntax = "proto3"; + +import "buf/validate/validate.proto"; +import "google/protobuf/timestamp.proto"; + +message EventFromTheNineteenthCentury { + google.protobuf.Timestamp time = 1 [(buf.validate.field).cel = { + id: "timestamp_in_the_1800s", + message: "the event must be from the nineteenth century", + expression: + // `timestamp()` converts a string to a timestamp according to [rtf3339] + // (https://datatracker.ietf.org/doc/html/rfc3339#section-5.8). + // The `<=`, `<`, `>`, `>=`, `==`, `!=` operators are overloaded for + // timestamps as well. Where 'less than' really means 'earlier than'. + // In this case, the expression validates that the field is after + // or equal to 1800-01-01T00:00:00+00:00 but before 1900-01-01T00:00:00+00:00. + "timestamp('1800-01-01T00:00:00+00:00') <= this" + "&& this < timestamp('1900-01-01T00:00:00+00:00')" + }]; +} diff --git a/examples/cel_timestamp_to_day_of_week.proto b/examples/cel_timestamp_get_attribute.proto similarity index 80% rename from examples/cel_timestamp_to_day_of_week.proto rename to examples/cel_timestamp_get_attribute.proto index a71cd3a6..047129d6 100644 --- a/examples/cel_timestamp_to_day_of_week.proto +++ b/examples/cel_timestamp_get_attribute.proto @@ -27,10 +27,14 @@ message BookHaircutAppointmentRequest { google.protobuf.Timestamp appointment_time = 1 [(buf.validate.field).cel = { id: "not_open_on_monday", message: "the barbershop is closed on Monday", - // `getDayOfWeek(