diff --git a/README.md b/README.md index c01515a6..8563e1d0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ [![PyPI version](https://badge.fury.io/py/protovalidate.svg)](https://badge.fury.io/py/protovalidate) `protovalidate-python` is the Python implementation of [`protovalidate`](https://github.com/bufbuild/protovalidate), -designed to validate Protobuf messages at runtime based on user-defined validation constraints. Powered by Google's +designed to validate Protobuf messages at runtime based on user-defined validation rules. Powered by Google's Common Expression Language ([CEL](https://github.com/google/cel-spec)), it provides a flexible and efficient foundation for defining and evaluating custom validation rules. The primary goal of `protovalidate` is to help developers ensure data consistency and integrity across the network without requiring generated code. @@ -15,7 +15,7 @@ data consistency and integrity across the network without requiring generated co Head over to the core [`protovalidate`](https://github.com/bufbuild/protovalidate/) repository for: - [The API definition](https://github.com/bufbuild/protovalidate/tree/main/proto/protovalidate/buf/validate/validate.proto): - used to describe validation constraints + used to describe validation rules. - [Documentation](https://github.com/bufbuild/protovalidate/tree/main/docs): how to apply `protovalidate` effectively - [Migration tooling](https://github.com/bufbuild/protovalidate/tree/main/docs/migrate.md): incrementally migrate from `protoc-gen-validate` @@ -45,9 +45,9 @@ project's [PyPI page](https://pypi.org/project/protovalidate/). ## Usage -### Implementing validation constraints +### Implementing validation rules -Validation constraints are defined directly within `.proto` files. Documentation for adding constraints can be found in +Validation rules are defined directly within `.proto` files. Documentation for adding rules can be found in the `protovalidate` project [README](https://github.com/bufbuild/protovalidate) and its [comprehensive docs](https://github.com/bufbuild/protovalidate/tree/main/docs). diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index cf211c75..09ba69e3 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -13,8 +13,8 @@ # limitations under the License. import math +import re import typing -from email.utils import parseaddr from ipaddress import IPv4Address, IPv4Network, IPv6Address, IPv6Network, ip_address, ip_network from urllib import parse as urlparse @@ -23,6 +23,11 @@ from protovalidate.internal import string_format +# See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address +_email_regex = re.compile( + r"^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$" +) + def _validate_hostname(host): if not host: @@ -49,23 +54,6 @@ def _validate_hostname(host): return not all_digits -def validate_email(addr): - parts = parseaddr(addr) - if addr != parts[1]: - return False - - addr = parts[1] - if len(addr) > 254: - return False - - parts = addr.split("@") - if len(parts) != 2: - return False - if len(parts[0]) > 64: - return False - return _validate_hostname(parts[1]) - - def validate_host_and_port(string: str, *, port_required: bool) -> bool: if not string: return False @@ -157,35 +145,75 @@ def is_ip_prefix(val: celtypes.Value, *args) -> celpy.Result: def is_email(string: celtypes.Value) -> celpy.Result: + """Validate whether string is a valid email address. + + Conforms to the definition for a valid email address from the HTML standard. + Note that this standard willfully deviates from RFC 5322, which allows many + unexpected forms of email addresses and will easily match a typographical + error. + + Args: + string (celTypes.Value): The string to validate. + + Returns: + True if the string is an email address, for example "foo@example.com". False otherwise. + + Raises: + celpy.CELEvalError: If string is not an instance of celtypes.StringType. + """ + if not isinstance(string, celtypes.StringType): msg = "invalid argument, expected string" raise celpy.CELEvalError(msg) - return celtypes.BoolType(validate_email(string)) + m = _email_regex.match(string) is not None + return celtypes.BoolType(m) def is_uri(string: celtypes.Value) -> celpy.Result: - url = urlparse.urlparse(str(string)) - # urlparse correctly reads the scheme from URNs but parses everything - # after (except the query string) as the path. - if url.scheme == "urn": - if not (url.path): - return celtypes.BoolType(False) - elif not all([url.scheme, url.netloc, url.path]): - return celtypes.BoolType(False) + """Validate whether string is a valid URI. + + URI is defined in the internet standard RFC 3986. + Zone Identifiers in IPv6 address literals are supported (RFC 6874). - # If the query string contains percent-encoding, then try to decode it. - # unquote will return the same string if it is improperly encoded. - if "%" in url.query: - return celtypes.BoolType(urlparse.unquote(url.query) != url.query) + Args: + string (celTypes.Value): The string to validate. - return celtypes.BoolType(True) + Returns: + True if the string is a URI, for example "https://example.com/foo/bar?baz=quux#frag". False otherwise. + + Raises: + celpy.CELEvalError: If string is not an instance of celtypes.StringType. + """ + + if not isinstance(string, celtypes.StringType): + msg = "invalid argument, expected string" + raise celpy.CELEvalError(msg) + valid = Uri(str(string)).uri() + return celtypes.BoolType(valid) def is_uri_ref(string: celtypes.Value) -> celpy.Result: - url = urlparse.urlparse(str(string)) - if not all([url.scheme, url.path]) and url.fragment: - return celtypes.BoolType(False) - return celtypes.BoolType(True) + """Validate whether string is a valid URI reference. + + URI, URI Reference, and Relative Reference are defined in the internet standard RFC 3986. + Zone Identifiers in IPv6 address literals are supported (RFC 6874). + + Args: + string (celTypes.Value): The string to validate. + + Returns: + True if the string is a URI Reference - a URI such as "https://example.com/foo/bar?baz=quux#frag" + or a Relative Reference such as "./foo/bar?query". False otherwise. + + Raises: + celpy.CELEvalError: If string is not an instance of celtypes.StringType. + """ + + if not isinstance(string, celtypes.StringType): + msg = "invalid argument, expected string" + raise celpy.CELEvalError(msg) + valid = Uri(str(string)).uri_reference() + return celtypes.BoolType(valid) def is_hostname(string: celtypes.Value) -> celpy.Result: @@ -237,6 +265,771 @@ def unique(val: celtypes.Value) -> celpy.Result: return celtypes.BoolType(len(val) == len(set(val))) +class Uri: + """Uri is a class used to parse a given string to determine if it is a valid URI or URI reference. + + Callers can validate a string by constructing an instance of this class and then calling one of its + public methods: + uri() + uri_reference() + + Each method will return True or False depending on whether it passes validation. + """ + + _string: str + _index: int + _pct_encoded_found: bool + + def __init__(self, string: str): + """Initialize a URI validation class with a given string + + Args: + string (str): String to validate as a URI or URI reference. + """ + + super().__init__() + self._string = string + self._index = 0 + + def uri(self) -> bool: + """Determine whether string is a valid URI. + + Method parses the rule: + + URI = scheme ":" hier-part [ "?" query ] [ "#" fragment ] + """ + + start = self._index + if not (self.__scheme() and self.__take(":") and self.__hier_part()): + self._index = start + return False + + if self.__take("?") and not self.__query(): + return False + + if self.__take("#") and not self.__fragment(): + return False + + if self._index != len(self._string): + self._index = start + return False + + return True + + def uri_reference(self) -> bool: + """Determine whether string is a valid URI reference. + + Method parses the rule: + + URI-reference = URI / relative-ref + """ + + return self.uri() or self.__relative_ref() + + def __hier_part(self) -> bool: + """Determine whether string contains a valid hier-part. + + Method parses the rule: + + hier-part = "//" authority path-abempty. + / path-absolute + / path-rootless + / path-empty + """ + + start = self._index + if self.__take("/") and self.__take("/") and self.__authority() and self.__path_abempty(): + return True + + self._index = start + + return self.__path_absolute() or self.__path_rootless() or self.__path_empty() + + def __relative_ref(self) -> bool: + """Determine whether string contains a valid relative reference. + + Method parses the rule: + + relative-ref = relative-part [ "?" query ] [ "#" fragment ] + """ + + start = self._index + if not self.__relative_part(): + return False + + if self.__take("?") and not self.__query(): + self._index = start + return False + + if self.__take("#") and not self.__fragment(): + self._index = start + return False + + if self._index != len(self._string): + self._index = start + return False + + return True + + def __relative_part(self) -> bool: + """Determine whether string contains a valid relative part. + + Method parses the rule: + + relative-part = "//" authority path-abempty + / path-absolute + / path-noscheme + / path-empty + """ + + start = self._index + if self.__take("/") and self.__take("/") and self.__authority() and self.__path_abempty(): + return True + + self._index = start + + return self.__path_absolute() or self.__path_noscheme() or self.__path_empty() + + def __scheme(self) -> bool: + """Determine whether string contains a valid scheme. + + Method parses the rule: + + scheme = ALPHA *( ALPHA / DIGIT / "+" / "-" / "." ) + + Terminated by ":". + """ + + start = self._index + if self.__alpha(): + while self.__alpha() or self.__digit() or self.__take("+") or self.__take("-") or self.__take("."): + pass + + if self._string[self._index] == ":": + return True + + self._index = start + return False + + def __authority(self) -> bool: + """Determine whether string contains a valid authority. + + Method parses the rule: + + authority = [ userinfo "@" ] host [ ":" port ] + + Lead by double slash ("") and terminated by "/", "?", "#", or end of URI. + """ + + start = self._index + if self.__userinfo(): + if not self.__take("@"): + self._index = start + return False + + if not self.__host(): + self._index = start + return False + + if self.__take(":"): + if not self.__port(): + self._index = start + return False + + if not self.__is_authority_end(): + self._index = start + return False + + return True + + def __is_authority_end(self) -> bool: + """Report whether the current position is the end of the authority. + + The authority component [...] is terminated by the next slash ("/"), + question mark ("?"), or number sign ("#") character, or by the + end of the URI. + """ + + return ( + self._index >= len(self._string) + or self._string[self._index] == "?" + or self._string[self._index] == "#" + or self._string[self._index] == "/" + ) + + def __userinfo(self) -> bool: + """Determine whether string contains a valid userinfo. + + Method parses the rule: + + userinfo = *( unreserved / pct-encoded / sub-delims / ":" ) + + Terminated by "@" in authority. + """ + + start = self._index + while True: + if self.__unreserved() or self.__pct_encoded() or self.__sub_delims() or self.__take(":"): + continue + + if self._index < len(self._string): + if self._string[self._index] == "@": + return True + + self._index = start + return False + + def __check_host_pct_encoded(self, string: str) -> bool: + """Verify that string is correctly percent-encoded""" + try: + # unquote defaults to 'UTF-8' encoding. + urlparse.unquote(string, errors="strict") + except UnicodeError: + return False + + return True + + def __host(self) -> bool: + """Determine whether string contains a valid host. + + Method parses the rule: + + host = IP-literal / IPv4address / reg-name. + """ + + if self._index >= len(self._string): + return False + + start = self._index + self._pct_encoded_found = False + + # Note: IPv4address is a subset of reg-name + if (self._string[self._index] == "[" and self.__ip_literal()) or self.__reg_name(): + if self._pct_encoded_found: + raw_host = self._string[start : self._index] + # RFC 3986: + # > URI producing applications must not use percent-encoding in host + # > unless it is used to represent a UTF-8 character sequence. + if not self.__check_host_pct_encoded(raw_host): + return False + + return True + + return False + + def __port(self) -> bool: + """Determine whether string contains a valid port. + + Method parses the rule: + + port = *DIGIT + + Terminated by end of authority. + """ + + start = self._index + while True: + if self.__digit(): + continue + + if self.__is_authority_end(): + return True + + self._index = start + return False + + def __ip_literal(self) -> bool: + """Determine whether string contains a valid port. + + Method parses the rule from RFC 6874: + + IP-literal = "[" ( IPv6address / IPv6addrz / IPvFuture ) "]" + """ + + start = self._index + + if self.__take("["): + curr_idx = self._index + if self.__ipv6_address() and self.__take("]"): + return True + + self._index = curr_idx + + if self.__ipv6_addrz() and self.__take("]"): + return True + + self._index = curr_idx + + if self.__ip_vfuture() and self.__take("]"): + return True + + self._index = start + return False + + def __ipv6_address(self) -> bool: + """Determine whether string contains a valid ipv6 address. + + Method parses the rule "IPv6address". + + Relies on the implementation of validate_ip. + """ + + start = self._index + while self.__hex_dig() or self.__take(":"): + pass + + if validate_ip(self._string[start : self._index], 6): + return True + + self._index = start + return False + + def __ipv6_addrz(self) -> bool: + """Determine whether string contains a valid IPv6addrz. + + Method parses the rule from RFC 6874: + + IPv6addrz = IPv6address "%25" ZoneID + """ + + start = self._index + if self.__ipv6_address() and self.__take("%") and self.__take("2") and self.__take("5") and self.__zone_id(): + return True + + self._index = start + + return False + + def __zone_id(self) -> bool: + """Determine whether string contains a valid zone ID. + + Method parses the rule from RFC 6874: + + ZoneID = 1*( unreserved / pct-encoded ) + """ + + start = self._index + while self.__unreserved() or self.__pct_encoded(): + pass + + if self._index - start > 0: + return True + + self._index = start + + return False + + def __ip_vfuture(self) -> bool: + """Determine whether string contains a valid ipvFuture. + + Method parses the rule: + + IPvFuture = "v" 1*HEXDIG "." 1*( unreserved / sub-delims / ":" ) + """ + + start = self._index + + if self.__take("v") and self.__hex_dig(): + while self.__hex_dig(): + pass + + if self.__take("."): + j = 0 + while self.__unreserved() or self.__sub_delims() or self.__take(":"): + j += 1 + + if j >= 1: + return True + + self._index = start + + return False + + def __reg_name(self) -> bool: + """Determine whether string contains a valid reg-name. + + Method parses the rule: + + reg-name = *( unreserved / pct-encoded / sub-delims ) + + Terminates on start of port (":") or end of authority. + """ + + start = self._index + while True: + if self.__unreserved() or self.__pct_encoded() or self.__sub_delims(): + continue + + if self.__is_authority_end(): + # End of authority + return True + + if self._string[self._index] == ":": + return True + + self._index = start + + return False + + def __is_path_end(self) -> bool: + """Determine whether the current index has reached the end of path. + + > The path is terminated by the first question mark ("?") or + > number sign ("#") character, or by the end of the URI. + """ + + return self._index >= len(self._string) or self._string[self._index] == "?" or self._string[self._index] == "#" + + def __path_abempty(self) -> bool: + """Determine whether string contains a path-abempty. + + Method parses the rule: + + path-abempty = *( "/" segment ) + + Terminated by end of path: "?", "#", or end of URI. + """ + + start = self._index + while self.__take("/") and self.__segment(): + pass + + if self.__is_path_end(): + return True + + self._index = start + + return False + + def __path_absolute(self) -> bool: + """Determine whether string contains a path-absolute. + + Method parses the rule: + + path-absolute = "/" [ segment-nz *( "/" segment ) ] + + Terminated by end of path: "?", "#", or end of URI. + """ + + start = self._index + + if self.__take("/"): + if self.__segment_nz(): + while self.__take("/") and self.__segment(): + pass + + if self.__is_path_end(): + return True + + self._index = start + + return False + + def __path_noscheme(self) -> bool: + """Determine whether string contains a path-noscheme. + + Method parses the rule: + + path-noscheme = segment-nz-nc *( "/" segment ) + + Terminated by end of path: "?", "#", or end of URI. + """ + + start = self._index + if self.__segment_nz_nc(): + while self.__take("/") and self.__segment(): + pass + + if self.__is_path_end(): + return True + + self._index = start + + return True + + def __path_rootless(self) -> bool: + """Determine whether string contains a path-rootless. + + Method parses the rule: + + path-rootless = segment-nz *( "/" segment ) + + Terminated by end of path: "?", "#", or end of URI. + """ + + start = self._index + + if self.__segment_nz(): + while self.__take("/") and self.__segment(): + pass + + if self.__is_path_end(): + return True + + self._index = start + + return True + + def __path_empty(self) -> bool: + """Determine whether string contains a path-empty. + + Method parses the rule: + + path-empty = 0 + + Terminated by end of path: "?", "#", or end of URI. + """ + + return self.__is_path_end() + + def __segment(self) -> bool: + """Determine whether string contains a segment. + + Method parses the rule: + + segment = *pchar + """ + + while self.__pchar(): + pass + + return True + + def __segment_nz(self) -> bool: + """Determine whether string contains a segment-nz. + + Method parses the rule: + + segment-nz = 1*pchar + """ + + start = self._index + + if self.__pchar(): + while self.__pchar(): + pass + + return True + + self._index = start + + return False + + def __segment_nz_nc(self) -> bool: + """Determine whether string contains a segment-nz-nc. + + Method parses the rule: + + segment-nz-nc = 1*( unreserved / pct-encoded / sub-delims / "@" ) + ; non-zero-length segment without any colon ":" + """ + + start = self._index + + while self.__unreserved() or self.__pct_encoded() or self.__sub_delims() or self.__take("@"): + pass + + if self._index - start > 0: + return True + + self._index = start + + return False + + def __pchar(self) -> bool: + """Report whether the current position is a pchar. + + Method parses the rule: + + pchar = unreserved / pct-encoded / sub-delims / ":" / "@" + """ + + return ( + self.__unreserved() or self.__pct_encoded() or self.__sub_delims() or self.__take(":") or self.__take("@") + ) + + def __query(self) -> bool: + """Determine whether string contains a valid query. + + Method parses the rule: + + query = *( pchar / "/" / "?" ) + + Terminated by "#" or end of URI. + """ + + start = self._index + + while True: + if self.__pchar() or self.__take("/") or self.__take("?"): + continue + + if self._index == len(self._string) or self._string[self._index] == "#": + return True + + self._index = start + + return False + + def __fragment(self) -> bool: + """Determine whether string contains a valid fragment. + + Method parses the rule: + + fragment = *( pchar / "/" / "?" ) + + Terminated by end of URI. + """ + + start = self._index + + while True: + if self.__pchar() or self.__take("/") or self.__take("?"): + continue + + if self._index == len(self._string): + return True + + self._index = start + + return False + + def __pct_encoded(self) -> bool: + """Determine whether string contains a valid percent encoding. + + Method parses the rule: + + pct-encoded = "%" HEXDIG HEXDIG + + Sets `_pct_encoded_found` to true if a valid triplet was found + """ + + start = self._index + + if self.__take("%") and self.__hex_dig() and self.__hex_dig(): + self._pct_encoded_found = True + return True + + self._index = start + + return False + + def __unreserved(self) -> bool: + """Report whether the current position is an unreserved character. + + Method parses the rule: + + unreserved = ALPHA / DIGIT / "-" / "." / "_" / "~" + """ + + return ( + self.__alpha() + or self.__digit() + or self.__take("-") + or self.__take("_") + or self.__take(".") + or self.__take("~") + ) + + def __sub_delims(self) -> bool: + """Report whether the current position is a sub-delim. + + Method parses the rule: + + sub-delims = "!" / "$" / "&" / "'" / "(" / ")" + / "*" / "+" / "," / ";" / "=" + """ + + return ( + self.__take("!") + or self.__take("$") + or self.__take("&") + or self.__take("'") + or self.__take("(") + or self.__take(")") + or self.__take("*") + or self.__take("+") + or self.__take(",") + or self.__take(";") + or self.__take("=") + ) + + def __alpha(self) -> bool: + """Report whether the current position is an alpha character. + + Method parses the rule: + + ALPHA = %x41-5A / %x61-7A ; A-Z / a-z + """ + + if self._index >= len(self._string): + return False + + c = self._string[self._index] + if ("A" <= c <= "Z") or ("a" <= c <= "z"): + self._index += 1 + return True + + return False + + def __digit(self) -> bool: + """Report whether the current position is a digit. + + Method parses the rule: + + DIGIT = %x30-39 ; 0-9 + """ + + if self._index >= len(self._string): + return False + + c = self._string[self._index] + if "0" <= c <= "9": + self._index += 1 + return True + + return False + + def __hex_dig(self) -> bool: + """Report whether the current position is a hex digit. + + Method parses the rule: + + HEXDIG = DIGIT / "A" / "B" / "C" / "D" / "E" / "F" + """ + + if self._index >= len(self._string): + return False + + c = self._string[self._index] + + if ("0" <= c <= "9") or ("a" <= c <= "f") or ("A" <= c <= "F") or ("0" <= c <= "9"): + self._index += 1 + + return True + + return False + + def __take(self, char: str) -> bool: + """Take the given char at the current index. + + If char is at the current index, increment the index. + + Returns: + True if char is at the current index. False if char is not at the + current index or the end of string has been reached. + """ + + if self._index >= len(self._string): + return False + + if self._string[self._index] == char: + self._index += 1 + return True + + return False + + def make_extra_funcs(locale: str) -> dict[str, celpy.CELFunction]: # TODO(#257): Fix types and add tests for StringFormat. # For now, ignoring the type. diff --git a/protovalidate/internal/constraints.py b/protovalidate/internal/rules.py similarity index 77% rename from protovalidate/internal/constraints.py rename to protovalidate/internal/rules.py index 2519108d..af71988b 100644 --- a/protovalidate/internal/constraints.py +++ b/protovalidate/internal/rules.py @@ -232,7 +232,7 @@ def _set_path_element_map_key( class Violation: - """A singular constraint violation.""" + """A singular rule violation.""" proto: validate_pb2.Violation field_value: typing.Any @@ -244,8 +244,8 @@ def __init__(self, *, field_value: typing.Any = None, rule_value: typing.Any = N self.rule_value = rule_value -class ConstraintContext: - """The state associated with a single constraint evaluation.""" +class RuleContext: + """The state associated with a single rule evaluation.""" def __init__(self, *, fail_fast: bool = False, violations: typing.Optional[list[Violation]] = None): self._fail_fast = fail_fast @@ -283,28 +283,28 @@ def has_errors(self) -> bool: return len(self._violations) > 0 def sub_context(self): - return ConstraintContext(fail_fast=self._fail_fast) + return RuleContext(fail_fast=self._fail_fast) -class ConstraintRules: - """The constraints associated with a single 'rules' message.""" +class RuleRules: + """The rules associated with a single 'rules' message.""" - def validate(self, ctx: ConstraintContext, _: message.Message): - """Validate the message against the rules in this constraint.""" - ctx.add(Violation(constraint_id="unimplemented", message="Unimplemented")) + def validate(self, ctx: RuleContext, _: message.Message): + """Validate the message against the rules in this rule.""" + ctx.add(Violation(rule_id="unimplemented", message="Unimplemented")) @dataclasses.dataclass class CelRunner: runner: celpy.Runner - constraint: validate_pb2.Constraint + rule: validate_pb2.Rule rule_value: typing.Optional[typing.Any] = None rule_cel: typing.Optional[celtypes.Value] = None rule_path: typing.Optional[validate_pb2.FieldPath] = None -class CelConstraintRules(ConstraintRules): - """A constraint that has rules written in CEL.""" +class CelRuleRules(RuleRules): + """A rule that has rules written in CEL.""" _cel: list[CelRunner] _rules: typing.Optional[message.Message] = None @@ -318,7 +318,7 @@ def __init__(self, rules: typing.Optional[message.Message]): def _validate_cel( self, - ctx: ConstraintContext, + ctx: RuleContext, *, this_value: typing.Optional[typing.Any] = None, this_cel: typing.Optional[celtypes.Value] = None, @@ -339,8 +339,8 @@ def _validate_cel( field_value=this_value, rule=cel.rule_path, rule_value=cel.rule_value, - constraint_id=cel.constraint.id, - message=cel.constraint.message, + rule_id=cel.rule.id, + message=cel.rule.message, for_key=for_key, ), ) @@ -351,7 +351,7 @@ def _validate_cel( field_value=this_value, rule=cel.rule_path, rule_value=cel.rule_value, - constraint_id=cel.constraint.id, + rule_id=cel.rule.id, message=result, for_key=for_key, ), @@ -363,7 +363,7 @@ def add_rule( self, env: celpy.Environment, funcs: dict[str, celpy.CELFunction], - rules: validate_pb2.Constraint, + rules: validate_pb2.Rule, *, rule_field: typing.Optional[descriptor.FieldDescriptor] = None, rule_path: typing.Optional[validate_pb2.FieldPath] = None, @@ -378,7 +378,7 @@ def add_rule( self._cel.append( CelRunner( runner=prog, - constraint=rules, + rule=rules, rule_value=rule_value, rule_cel=rule_cel, rule_path=rule_path, @@ -386,10 +386,10 @@ def add_rule( ) -class MessageConstraintRules(CelConstraintRules): +class MessageRuleRules(CelRuleRules): """Message-level rules.""" - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): self._validate_cel(ctx, this_cel=_msg_to_cel(message)) @@ -420,7 +420,7 @@ def _zero_value(field: descriptor.FieldDescriptor): return _field_value_to_cel(field.default_value, field) -class FieldConstraintRules(CelConstraintRules): +class FieldRuleRules(CelRuleRules): """Field-level rules.""" _ignore_empty = False @@ -431,9 +431,7 @@ class FieldConstraintRules(CelConstraintRules): _required_rule_path: typing.ClassVar[validate_pb2.FieldPath] = validate_pb2.FieldPath( elements=[ _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[ - validate_pb2.FieldConstraints.REQUIRED_FIELD_NUMBER - ] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.REQUIRED_FIELD_NUMBER] ) ] ) @@ -441,9 +439,7 @@ class FieldConstraintRules(CelConstraintRules): _cel_rule_path: typing.ClassVar[validate_pb2.FieldPath] = validate_pb2.FieldPath( elements=[ _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[ - validate_pb2.FieldConstraints.CEL_FIELD_NUMBER - ] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.CEL_FIELD_NUMBER] ) ] ) @@ -453,7 +449,7 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - field_level: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldRules, *, for_items: bool = False, ): @@ -474,7 +470,7 @@ def __init__( type_case = field_level.WhichOneof("type") if type_case is not None: rules: message.Message = getattr(field_level, type_case) - # For each set field in the message, look for the private constraint + # For each set field in the message, look for the private rule # extension. for list_field, _ in rules.ListFields(): if validate_pb2.predefined in list_field.GetOptions().Extensions: @@ -499,7 +495,7 @@ def __init__( rule_path.elements[0].index = i self.add_rule(env, funcs, cel, rule_path=rule_path) - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): if _is_empty_field(message, self._field): if self._required: ctx.add( @@ -509,9 +505,9 @@ def validate(self, ctx: ConstraintContext, message: message.Message): _field_to_element(self._field), ], ), - rule=FieldConstraintRules._required_rule_path, + rule=FieldRuleRules._required_rule_path, rule_value=self._required, - constraint_id="required", + rule_id="required", message="value is required", ), ) @@ -530,24 +526,22 @@ def validate(self, ctx: ConstraintContext, message: message.Message): sub_ctx.add_field_path_element(element) ctx.add_errors(sub_ctx) - def validate_item(self, ctx: ConstraintContext, val: typing.Any, *, for_key: bool = False): + def validate_item(self, ctx: RuleContext, val: typing.Any, *, for_key: bool = False): self._validate_value(ctx, val, for_key=for_key) self._validate_cel(ctx, this_value=val, this_cel=_scalar_field_value_to_cel(val, self._field), for_key=for_key) - def _validate_value(self, ctx: ConstraintContext, val: typing.Any, *, for_key: bool = False): + def _validate_value(self, ctx: RuleContext, val: typing.Any, *, for_key: bool = False): pass -class AnyConstraintRules(FieldConstraintRules): +class AnyRuleRules(FieldRuleRules): """Rules for an Any field.""" _in_rule_path: typing.ClassVar[validate_pb2.FieldPath] = validate_pb2.FieldPath( elements=[ _field_to_element(validate_pb2.AnyRules.DESCRIPTOR.fields_by_number[validate_pb2.AnyRules.IN_FIELD_NUMBER]), _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[ - validate_pb2.FieldConstraints.ANY_FIELD_NUMBER - ] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.ANY_FIELD_NUMBER] ), ], ) @@ -558,9 +552,7 @@ class AnyConstraintRules(FieldConstraintRules): validate_pb2.AnyRules.DESCRIPTOR.fields_by_number[validate_pb2.AnyRules.NOT_IN_FIELD_NUMBER] ), _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[ - validate_pb2.FieldConstraints.ANY_FIELD_NUMBER - ] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.ANY_FIELD_NUMBER] ), ], ) @@ -570,7 +562,7 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - field_level: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldRules, ): super().__init__(env, funcs, field, field_level) self._in = [] @@ -580,14 +572,14 @@ def __init__( if field_level.any.not_in: self._not_in = field_level.any.not_in - def _validate_value(self, ctx: ConstraintContext, value: any_pb2.Any, *, for_key: bool = False): + def _validate_value(self, ctx: RuleContext, value: any_pb2.Any, *, for_key: bool = False): if len(self._in) > 0: if value.type_url not in self._in: ctx.add( Violation( - rule=AnyConstraintRules._in_rule_path, + rule=AnyRuleRules._in_rule_path, rule_value=self._in, - constraint_id="any.in", + rule_id="any.in", message="type URL must be in the allow list", for_key=for_key, ) @@ -595,16 +587,16 @@ def _validate_value(self, ctx: ConstraintContext, value: any_pb2.Any, *, for_key if value.type_url in self._not_in: ctx.add( Violation( - rule=AnyConstraintRules._not_in_rule_path, + rule=AnyRuleRules._not_in_rule_path, rule_value=self._not_in, - constraint_id="any.not_in", + rule_id="any.not_in", message="type URL must not be in the block list", for_key=for_key, ) ) -class EnumConstraintRules(FieldConstraintRules): +class EnumRuleRules(FieldRuleRules): """Rules for an enum field.""" _defined_only = False @@ -615,9 +607,7 @@ class EnumConstraintRules(FieldConstraintRules): validate_pb2.EnumRules.DESCRIPTOR.fields_by_number[validate_pb2.EnumRules.DEFINED_ONLY_FIELD_NUMBER] ), _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[ - validate_pb2.FieldConstraints.ENUM_FIELD_NUMBER - ] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.ENUM_FIELD_NUMBER] ), ], ) @@ -627,7 +617,7 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - field_level: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldRules, *, for_items: bool = False, ): @@ -635,7 +625,7 @@ def __init__( if field_level.enum.defined_only: self._defined_only = True - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): super().validate(ctx, message) if ctx.done: return @@ -649,27 +639,25 @@ def validate(self, ctx: ConstraintContext, message: message.Message): _field_to_element(self._field), ], ), - rule=EnumConstraintRules._defined_only_rule_path, + rule=EnumRuleRules._defined_only_rule_path, rule_value=self._defined_only, - constraint_id="enum.defined_only", + rule_id="enum.defined_only", message="value must be one of the defined enum values", ), ) -class RepeatedConstraintRules(FieldConstraintRules): +class RepeatedRuleRules(FieldRuleRules): """Rules for a repeated field.""" - _item_rules: typing.Optional[FieldConstraintRules] = None + _item_rules: typing.Optional[FieldRuleRules] = None _items_rules_suffix: typing.ClassVar[list[validate_pb2.FieldPathElement]] = [ _field_to_element( validate_pb2.RepeatedRules.DESCRIPTOR.fields_by_number[validate_pb2.RepeatedRules.ITEMS_FIELD_NUMBER] ), _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[ - validate_pb2.FieldConstraints.REPEATED_FIELD_NUMBER - ] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.REPEATED_FIELD_NUMBER] ), ] @@ -678,14 +666,14 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - field_level: validate_pb2.FieldConstraints, - item_rules: typing.Optional[FieldConstraintRules], + field_level: validate_pb2.FieldRules, + item_rules: typing.Optional[FieldRuleRules], ): super().__init__(env, funcs, field, field_level) if item_rules is not None: self._item_rules = item_rules - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): super().validate(ctx, message) if ctx.done: return @@ -700,29 +688,29 @@ def validate(self, ctx: ConstraintContext, message: message.Message): element = _field_to_element(self._field) element.index = i sub_ctx.add_field_path_element(element) - sub_ctx.add_rule_path_elements(RepeatedConstraintRules._items_rules_suffix) + sub_ctx.add_rule_path_elements(RepeatedRuleRules._items_rules_suffix) ctx.add_errors(sub_ctx) if ctx.done: return -class MapConstraintRules(FieldConstraintRules): +class MapRuleRules(FieldRuleRules): """Rules for a map field.""" - _key_rules: typing.Optional[FieldConstraintRules] = None - _value_rules: typing.Optional[FieldConstraintRules] = None + _key_rules: typing.Optional[FieldRuleRules] = None + _value_rules: typing.Optional[FieldRuleRules] = None _key_rules_suffix: typing.ClassVar[list[validate_pb2.FieldPathElement]] = [ _field_to_element(validate_pb2.MapRules.DESCRIPTOR.fields_by_number[validate_pb2.MapRules.KEYS_FIELD_NUMBER]), _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[validate_pb2.FieldConstraints.MAP_FIELD_NUMBER] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.MAP_FIELD_NUMBER] ), ] _value_rules_suffix: typing.ClassVar[list[validate_pb2.FieldPathElement]] = [ _field_to_element(validate_pb2.MapRules.DESCRIPTOR.fields_by_number[validate_pb2.MapRules.VALUES_FIELD_NUMBER]), _field_to_element( - validate_pb2.FieldConstraints.DESCRIPTOR.fields_by_number[validate_pb2.FieldConstraints.MAP_FIELD_NUMBER] + validate_pb2.FieldRules.DESCRIPTOR.fields_by_number[validate_pb2.FieldRules.MAP_FIELD_NUMBER] ), ] @@ -731,9 +719,9 @@ def __init__( env: celpy.Environment, funcs: dict[str, celpy.CELFunction], field: descriptor.FieldDescriptor, - field_level: validate_pb2.FieldConstraints, - key_rules: typing.Optional[FieldConstraintRules], - value_rules: typing.Optional[FieldConstraintRules], + field_level: validate_pb2.FieldRules, + key_rules: typing.Optional[FieldRuleRules], + value_rules: typing.Optional[FieldRuleRules], ): super().__init__(env, funcs, field, field_level) if key_rules is not None: @@ -741,7 +729,7 @@ def __init__( if value_rules is not None: self._value_rules = value_rules - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): super().validate(ctx, message) if ctx.done: return @@ -752,13 +740,13 @@ def validate(self, ctx: ConstraintContext, message: message.Message): if not self._key_rules._ignore_empty or k: self._key_rules.validate_item(key_ctx, k, for_key=True) if key_ctx.has_errors(): - key_ctx.add_rule_path_elements(MapConstraintRules._key_rules_suffix) + key_ctx.add_rule_path_elements(MapRuleRules._key_rules_suffix) map_ctx = ctx.sub_context() if self._value_rules is not None: if not self._value_rules._ignore_empty or v: self._value_rules.validate_item(map_ctx, v) if map_ctx.has_errors(): - map_ctx.add_rule_path_elements(MapConstraintRules._value_rules_suffix) + map_ctx.add_rule_path_elements(MapRuleRules._value_rules_suffix) map_ctx.add_errors(key_ctx) if map_ctx.has_errors(): element = _field_to_element(self._field) @@ -769,17 +757,17 @@ def validate(self, ctx: ConstraintContext, message: message.Message): ctx.add_errors(map_ctx) -class OneofConstraintRules(ConstraintRules): +class OneofRuleRules(RuleRules): """Rules for a oneof definition.""" required = True - def __init__(self, oneof: descriptor.OneofDescriptor, rules: validate_pb2.OneofConstraints): + def __init__(self, oneof: descriptor.OneofDescriptor, rules: validate_pb2.OneofRules): self._oneof = oneof if not rules.required: self.required = False - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): if not message.WhichOneof(self._oneof.name): if self.required: ctx.add( @@ -787,29 +775,29 @@ def validate(self, ctx: ConstraintContext, message: message.Message): field=validate_pb2.FieldPath( elements=[_oneof_to_element(self._oneof)], ), - constraint_id="required", + rule_id="required", message="exactly one field is required in oneof", ) ) return -class ConstraintFactory: - """Factory for creating and caching constraints.""" +class RuleFactory: + """Factory for creating and caching rules.""" _env: celpy.Environment _funcs: dict[str, celpy.CELFunction] - _cache: dict[descriptor.Descriptor, typing.Union[list[ConstraintRules], Exception]] + _cache: dict[descriptor.Descriptor, typing.Union[list[RuleRules], Exception]] def __init__(self, funcs: dict[str, celpy.CELFunction]): self._env = celpy.Environment(runner_class=InterpretedRunner) self._funcs = funcs self._cache = {} - def get(self, descriptor: descriptor.Descriptor) -> list[ConstraintRules]: + def get(self, descriptor: descriptor.Descriptor) -> list[RuleRules]: if descriptor not in self._cache: try: - self._cache[descriptor] = self._new_constraints(descriptor) + self._cache[descriptor] = self._new_rules(descriptor) except Exception as e: self._cache[descriptor] = e result = self._cache[descriptor] @@ -817,16 +805,16 @@ def get(self, descriptor: descriptor.Descriptor) -> list[ConstraintRules]: raise result return result - def _new_message_constraint(self, rules: validate_pb2.MessageConstraints) -> MessageConstraintRules: - result = MessageConstraintRules(rules) + def _new_message_rule(self, rules: validate_pb2.MessageRules) -> MessageRuleRules: + result = MessageRuleRules(rules) for cel in rules.cel: result.add_rule(self._env, self._funcs, cel) return result - def _new_scalar_field_constraint( + def _new_scalar_field_rule( self, field: descriptor.FieldDescriptor, - field_level: validate_pb2.FieldConstraints, + field_level: validate_pb2.FieldRules, *, for_items: bool = False, ): @@ -834,23 +822,23 @@ def _new_scalar_field_constraint( return None type_case = field_level.WhichOneof("type") if type_case is None: - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "duration": check_field_type(field, 0, "google.protobuf.Duration") - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "timestamp": check_field_type(field, 0, "google.protobuf.Timestamp") - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "enum": check_field_type(field, descriptor.FieldDescriptor.TYPE_ENUM) - result = EnumConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = EnumRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "bool": check_field_type(field, descriptor.FieldDescriptor.TYPE_BOOL, "google.protobuf.BoolValue") - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "bytes": check_field_type( @@ -858,15 +846,15 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_BYTES, "google.protobuf.BytesValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "fixed32": check_field_type(field, descriptor.FieldDescriptor.TYPE_FIXED32) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "fixed64": check_field_type(field, descriptor.FieldDescriptor.TYPE_FIXED64) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "float": check_field_type( @@ -874,7 +862,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_FLOAT, "google.protobuf.FloatValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "double": check_field_type( @@ -882,7 +870,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_DOUBLE, "google.protobuf.DoubleValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "int32": check_field_type( @@ -890,7 +878,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_INT32, "google.protobuf.Int32Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "int64": check_field_type( @@ -898,23 +886,23 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_INT64, "google.protobuf.Int64Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "sfixed32": check_field_type(field, descriptor.FieldDescriptor.TYPE_SFIXED32) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "sfixed64": check_field_type(field, descriptor.FieldDescriptor.TYPE_SFIXED64) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "sint32": check_field_type(field, descriptor.FieldDescriptor.TYPE_SINT32) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "sint64": check_field_type(field, descriptor.FieldDescriptor.TYPE_SINT64) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "uint32": check_field_type( @@ -922,7 +910,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_UINT32, "google.protobuf.UInt32Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "uint64": check_field_type( @@ -930,7 +918,7 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_UINT64, "google.protobuf.UInt64Value", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "string": check_field_type( @@ -938,56 +926,56 @@ def _new_scalar_field_constraint( descriptor.FieldDescriptor.TYPE_STRING, "google.protobuf.StringValue", ) - result = FieldConstraintRules(self._env, self._funcs, field, field_level, for_items=for_items) + result = FieldRuleRules(self._env, self._funcs, field, field_level, for_items=for_items) return result elif type_case == "any": check_field_type(field, descriptor.FieldDescriptor.TYPE_MESSAGE, "google.protobuf.Any") - result = AnyConstraintRules(self._env, self._funcs, field, field_level) + result = AnyRuleRules(self._env, self._funcs, field, field_level) return result - def _new_field_constraint( + def _new_field_rule( self, field: descriptor.FieldDescriptor, - rules: validate_pb2.FieldConstraints, - ) -> FieldConstraintRules: + rules: validate_pb2.FieldRules, + ) -> FieldRuleRules: if field.label != descriptor.FieldDescriptor.LABEL_REPEATED: - return self._new_scalar_field_constraint(field, rules) + return self._new_scalar_field_rule(field, rules) if field.message_type is not None and field.message_type.GetOptions().map_entry: key_rules = None if rules.map.HasField("keys"): key_field = field.message_type.fields_by_name["key"] - key_rules = self._new_scalar_field_constraint(key_field, rules.map.keys, for_items=True) + key_rules = self._new_scalar_field_rule(key_field, rules.map.keys, for_items=True) value_rules = None if rules.map.HasField("values"): value_field = field.message_type.fields_by_name["value"] - value_rules = self._new_scalar_field_constraint(value_field, rules.map.values, for_items=True) - return MapConstraintRules(self._env, self._funcs, field, rules, key_rules, value_rules) + value_rules = self._new_scalar_field_rule(value_field, rules.map.values, for_items=True) + return MapRuleRules(self._env, self._funcs, field, rules, key_rules, value_rules) item_rule = None if rules.repeated.HasField("items"): - item_rule = self._new_scalar_field_constraint(field, rules.repeated.items) - return RepeatedConstraintRules(self._env, self._funcs, field, rules, item_rule) + item_rule = self._new_scalar_field_rule(field, rules.repeated.items) + return RepeatedRuleRules(self._env, self._funcs, field, rules, item_rule) - def _new_constraints(self, desc: descriptor.Descriptor) -> list[ConstraintRules]: - result: list[ConstraintRules] = [] - constraint: typing.Optional[ConstraintRules] = None + def _new_rules(self, desc: descriptor.Descriptor) -> list[RuleRules]: + result: list[RuleRules] = [] + rule: typing.Optional[RuleRules] = None if validate_pb2.message in desc.GetOptions().Extensions: message_level = desc.GetOptions().Extensions[validate_pb2.message] if message_level.disabled: return [] - if constraint := self._new_message_constraint(message_level): - result.append(constraint) + if rule := self._new_message_rule(message_level): + result.append(rule) for oneof in desc.oneofs: if validate_pb2.oneof in oneof.GetOptions().Extensions: - if constraint := OneofConstraintRules(oneof, oneof.GetOptions().Extensions[validate_pb2.oneof]): - result.append(constraint) + if rule := OneofRuleRules(oneof, oneof.GetOptions().Extensions[validate_pb2.oneof]): + result.append(rule) for field in desc.fields: if validate_pb2.field in field.GetOptions().Extensions: field_level = field.GetOptions().Extensions[validate_pb2.field] if field_level.ignore == validate_pb2.IGNORE_ALWAYS: continue - result.append(self._new_field_constraint(field, field_level)) + result.append(self._new_field_rule(field, field_level)) if field_level.repeated.items.ignore == validate_pb2.IGNORE_ALWAYS: continue if field.message_type is None: @@ -997,43 +985,43 @@ def _new_constraints(self, desc: descriptor.Descriptor) -> list[ConstraintRules] value_field = field.message_type.fields_by_name["value"] if value_field.type != descriptor.FieldDescriptor.TYPE_MESSAGE: continue - result.append(MapValMsgConstraint(self, field, key_field, value_field)) + result.append(MapValMsgRule(self, field, key_field, value_field)) elif field.label == descriptor.FieldDescriptor.LABEL_REPEATED: - result.append(RepeatedMsgConstraint(self, field)) + result.append(RepeatedMsgRule(self, field)) else: - result.append(SubMsgConstraint(self, field)) + result.append(SubMsgRule(self, field)) return result -class SubMsgConstraint(ConstraintRules): +class SubMsgRule(RuleRules): def __init__( self, - factory: ConstraintFactory, + factory: RuleFactory, field: descriptor.FieldDescriptor, ): self._factory = factory self._field = field - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): if not message.HasField(self._field.name): return - constraints = self._factory.get(self._field.message_type) - if constraints is None: + rules = self._factory.get(self._field.message_type) + if rules is None: return val = getattr(message, self._field.name) sub_ctx = ctx.sub_context() - for constraint in constraints: - constraint.validate(sub_ctx, val) + for rule in rules: + rule.validate(sub_ctx, val) if sub_ctx.has_errors(): element = _field_to_element(self._field) sub_ctx.add_field_path_element(element) ctx.add_errors(sub_ctx) -class MapValMsgConstraint(ConstraintRules): +class MapValMsgRule(RuleRules): def __init__( self, - factory: ConstraintFactory, + factory: RuleFactory, field: descriptor.FieldDescriptor, key_field: descriptor.FieldDescriptor, value_field: descriptor.FieldDescriptor, @@ -1043,17 +1031,17 @@ def __init__( self._key_field = key_field self._value_field = value_field - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): val = getattr(message, self._field.name) if not val: return - constraints = self._factory.get(self._value_field.message_type) - if constraints is None: + rules = self._factory.get(self._value_field.message_type) + if rules is None: return for k, v in val.items(): sub_ctx = ctx.sub_context() - for constraint in constraints: - constraint.validate(sub_ctx, v) + for rule in rules: + rule.validate(sub_ctx, v) if sub_ctx.has_errors(): element = _field_to_element(self._field) _set_path_element_map_key(element, k, self._key_field, self._value_field) @@ -1061,26 +1049,26 @@ def validate(self, ctx: ConstraintContext, message: message.Message): ctx.add_errors(sub_ctx) -class RepeatedMsgConstraint(ConstraintRules): +class RepeatedMsgRule(RuleRules): def __init__( self, - factory: ConstraintFactory, + factory: RuleFactory, field: descriptor.FieldDescriptor, ): self._factory = factory self._field = field - def validate(self, ctx: ConstraintContext, message: message.Message): + def validate(self, ctx: RuleContext, message: message.Message): val = getattr(message, self._field.name) if not val: return - constraints = self._factory.get(self._field.message_type) - if constraints is None: + rules = self._factory.get(self._field.message_type) + if rules is None: return for idx, item in enumerate(val): sub_ctx = ctx.sub_context() - for constraint in constraints: - constraint.validate(sub_ctx, item) + for rule in rules: + rule.validate(sub_ctx, item) if sub_ctx.has_errors(): element = _field_to_element(self._field) element.index = idx diff --git a/protovalidate/validator.py b/protovalidate/validator.py index 45fffa01..b53b3e4f 100644 --- a/protovalidate/validator.py +++ b/protovalidate/validator.py @@ -17,27 +17,27 @@ from google.protobuf import message from buf.validate import validate_pb2 # type: ignore -from protovalidate.internal import constraints as _constraints from protovalidate.internal import extra_func +from protovalidate.internal import rules as _rules -CompilationError = _constraints.CompilationError +CompilationError = _rules.CompilationError Violations = validate_pb2.Violations -Violation = _constraints.Violation +Violation = _rules.Violation class Validator: """ - Validates protobuf messages against static constraints. + Validates protobuf messages against static rules. Each validator instance caches internal state generated from the static - constraints, so reusing the same instance for multiple validations + rules, so reusing the same instance for multiple validations significantly improves performance. """ - _factory: _constraints.ConstraintFactory + _factory: _rules.RuleFactory def __init__(self): - self._factory = _constraints.ConstraintFactory(extra_func.EXTRA_FUNCS) + self._factory = _rules.RuleFactory(extra_func.EXTRA_FUNCS) def validate( self, @@ -46,14 +46,14 @@ def validate( fail_fast: bool = False, ): """ - Validates the given message against the static constraints defined in + Validates the given message against the static rules defined in the message's descriptor. Parameters: message: The message to validate. fail_fast: If true, validation will stop after the first violation. Raises: - CompilationError: If the static constraints could not be compiled. + CompilationError: If the static rules could not be compiled. ValidationError: If the message is invalid. """ violations = self.collect_violations(message, fail_fast=fail_fast) @@ -69,7 +69,7 @@ def collect_violations( into: typing.Optional[list[Violation]] = None, ) -> list[Violation]: """ - Validates the given message against the static constraints defined in + Validates the given message against the static rules defined in the message's descriptor. Compared to validate, collect_violations is faster but puts the burden of raising an appropriate exception on the caller. @@ -80,11 +80,11 @@ def collect_violations( into: If provided, any violations will be appended to the Violations object and the same object will be returned. Raises: - CompilationError: If the static constraints could not be compiled. + CompilationError: If the static rules could not be compiled. """ - ctx = _constraints.ConstraintContext(fail_fast=fail_fast, violations=into) - for constraint in self._factory.get(message.DESCRIPTOR): - constraint.validate(ctx, message) + ctx = _rules.RuleContext(fail_fast=fail_fast, violations=into) + for rule in self._factory.get(message.DESCRIPTOR): + rule.validate(ctx, message) if ctx.done: break for violation in ctx.violations: @@ -100,9 +100,9 @@ class ValidationError(ValueError): An error raised when a message fails to validate. """ - _violations: list[_constraints.Violation] + _violations: list[_rules.Violation] - def __init__(self, msg: str, violations: list[_constraints.Violation]): + def __init__(self, msg: str, violations: list[_rules.Violation]): super().__init__(msg) self._violations = violations diff --git a/tests/conformance/nonconforming.yaml b/tests/conformance/nonconforming.yaml index 531cf553..9fddb175 100644 --- a/tests/conformance/nonconforming.yaml +++ b/tests/conformance/nonconforming.yaml @@ -1,70 +1,30 @@ # celpy doesn't support nano seconds # ref: https://github.com/cloud-custodian/cel-python/issues/43 -standard_constraints/well_known_types/duration: +standard_rules/well_known_types/duration: - gte_lte/invalid/above - lte/invalid - not in/valid -standard_constraints/well_known_types/timestamp: +standard_rules/well_known_types/timestamp: - gte_lte/invalid/above - lte/invalid -library/is_email: - - invalid/left_side_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/non_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"ยต@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/quoted-string/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"\"foo..bar\"@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - invalid/trailing_dot - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"foo@example.com."} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # got: valid - - valid/exhaust_atext - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ!#$%&'*+-/=?^_`{|}~@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - # for_key: false - - valid/label_all_digits - # input: [type.googleapis.com/buf.validate.conformance.cases.IsEmail]:{val:"foo@0.1.2.3.4.5.6.7.8.9"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_email" - # message: "" - # for_key: false library/is_host_and_port: - port_required/false/invalid/port_number_sign # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"example.com:+0"} # want: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" + # 1. rule_id: "library.is_host_and_port" # got: valid - port_required/false/valid/ipv6_zone-id_any_non_null_character # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"[::1%% :x\x1f]"} # want: valid # got: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" + # 1. rule_id: "library.is_host_and_port" # message: "" # for_key: false - port_required/true/invalid/port_number_sign # input: [type.googleapis.com/buf.validate.conformance.cases.IsHostAndPort]:{val:"example.com:+0" port_required:true} # want: validation error (1 violation) - # 1. constraint_id: "library.is_host_and_port" + # 1. rule_id: "library.is_host_and_port" # got: valid library/is_ip: - version/0/valid/ipv4 @@ -78,43 +38,43 @@ library/is_ip: - version/1/invalid/empty_string # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{version:1} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # got: runtime error: invalid argument, expected 4 or 6 - version/1/invalid/ipv4 # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"127.0.0.1" version:1} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # got: runtime error: invalid argument, expected 4 or 6 - version/1/invalid/ipv6 # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"::1" version:1} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # got: runtime error: invalid argument, expected 4 or 6 - version/5/invalid/ipv4 # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"127.0.0.1" version:5} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # got: runtime error: invalid argument, expected 4 or 6 - version/5/invalid/ipv6 # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"::1" version:5} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # got: runtime error: invalid argument, expected 4 or 6 - version/7/invalid/ipv4 # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"127.0.0.1" version:7} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # got: runtime error: invalid argument, expected 4 or 6 - version/7/invalid/ipv6 # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"::1" version:7} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # got: runtime error: invalid argument, expected 4 or 6 - version/omitted/valid/ipv6_zone-id_any_non_null_character # input: [type.googleapis.com/buf.validate.conformance.cases.IsIp]:{val:"::1%% :x\x1f"} # want: valid # got: validation error (1 violation) - # 1. constraint_id: "library.is_ip" + # 1. rule_id: "library.is_ip" # message: "" # for_key: false library/is_ip_prefix: @@ -129,936 +89,55 @@ library/is_ip_prefix: - version/1/strict/omitted/invalid/empty_string # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{version:1} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" + # 1. rule_id: "library.is_ip_prefix" # got: runtime error: invalid argument, expected 4 or 6 - version/5/strict/omitted/invalid/ipv6_prefix # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"::1/64" version:5} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" + # 1. rule_id: "library.is_ip_prefix" # got: runtime error: invalid argument, expected 4 or 6 - version/7/strict/omitted/invalid/ipv6_prefix # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"::1/64" version:7} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" + # 1. rule_id: "library.is_ip_prefix" # got: runtime error: invalid argument, expected 4 or 6 - version/omitted/strict/omitted/invalid/ipv4_bad_leading_zero_in_prefix-length # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"192.168.1.0/024"} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" + # 1. rule_id: "library.is_ip_prefix" # got: valid - version/omitted/strict/omitted/invalid/ipv4_missing_prefix # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"192.168.1.0"} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" + # 1. rule_id: "library.is_ip_prefix" # got: valid - version/omitted/strict/omitted/invalid/ipv6_bad_leading_zero_in_prefix-length # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF/024"} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" + # 1. rule_id: "library.is_ip_prefix" # got: valid - version/omitted/strict/omitted/invalid/ipv6_missing_prefix # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"2001:0DB8:ABCD:0012:FFFF:FFFF:FFFF:FFFF"} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" + # 1. rule_id: "library.is_ip_prefix" # got: valid - version/omitted/strict/omitted/invalid/ipv6_zone-id/a # input: [type.googleapis.com/buf.validate.conformance.cases.IsIpPrefix]:{val:"::1%en1/64"} # want: validation error (1 violation) - # 1. constraint_id: "library.is_ip_prefix" - # got: valid -library/is_uri: - - invalid/authority_path-abempty_segment_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo://example.com/^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/authority_path-abempty_segment_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo://example.com/\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" + # 1. rule_id: "library.is_ip_prefix" # got: valid - - invalid/authority_path-abempty_segment_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo://example.com/%x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: valid - - invalid/host_ipfuture - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[v1x]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: runtime error: ('return error for overflow', , ('IPvFuture address is invalid',)) - - invalid/host_ipv6/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[2001::0370::7334]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: runtime error: ('return error for overflow', , ("'2001::0370::7334' does not appear to be an IPv4 or IPv6 address",)) - - invalid/host_ipv6_zone-id_bad_pct-encoded/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: runtime error: ('return error for overflow', , ("'::1%25foo%' does not appear to be an IPv4 or IPv6 address",)) - - invalid/host_ipv6_zone-id_bad_pct-encoded/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%2x]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: runtime error: ('return error for overflow', , ("'::1%25foo%2x' does not appear to be an IPv4 or IPv6 address",)) - - invalid/host_ipv6_zone-id_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%c3x%96]"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: runtime error: ('return error for overflow', , ("'::1%25foo%c3x%96' does not appear to be an IPv4 or IPv6 address",)) - - invalid/userinfo_reserved_square_bracket_close - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://]@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: runtime error: ('return error for overflow', , ('Invalid IPv6 URL',)) - - invalid/userinfo_reserved_square_bracket_open - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[@example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # got: runtime error: ('return error for overflow', , ('Invalid IPv6 URL',)) - - valid/authority_path-abempty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo://example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/example - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/fragment_pchar_extra - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com#/?"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/fragment_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com#%61%20%23"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/fragment_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com#%c3x%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/fragment_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com#%c3%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/fragment_sub-delims - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com#!$&'()*+,;="} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ip4v_bad_octet - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://256.0.0.1"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ipfuture_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[vF.-!$&'()*+,;=._~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ipfuture_long - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[v1234AF.x]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ipfuture_short - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[v1.x]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ipv4 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://127.0.0.1"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ipv6 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[2001:0db8:85a3:0000:0000:8a2e:0370:7334]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ipv6_zone-id - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25eth0]"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_ipv6_zone-id_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%61%20%23]"} - # want: valid - # got: runtime error: ('return error for overflow', , ("'::1%25foo%61%20%23' does not appear to be an IPv4 or IPv6 address",)) - - valid/host_ipv6_zone-id_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://[::1%25foo%c3%96]"} - # want: valid - # got: runtime error: ('return error for overflow', , ("'::1%25foo%c3%96' does not appear to be an IPv4 or IPv6 address",)) - - valid/host_reg-name - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_reg-name_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://:8080"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_reg-name_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://!$&'()*+,;=._~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_reg-name_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo%61%20%23"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/host_reg-name_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://foo%c3%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_exhaust_segment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ%20!$&'()*+,;=:@%20"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_exhaust_segment-nz - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/@%20!$&()*+,;=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~:"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_segment-nz-pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/%61%20%23"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_segment-nz-pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/%c3x%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_segment-nz-pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/%c3%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_segment_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz/%61%20%23"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_segment_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz/%c3x%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_segment_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz/%c3%96%c3"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_with_empty_pchar - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_with_query_and_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz?q#f"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_with_segment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz/a"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-absolute_with_segments - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:/nz//segment//segment/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-empty_with_query_and_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:?q#f"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment-nz_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:@%20!$&()*+,;=0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~:"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment-nz_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:%61%20%23"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment-nz_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:%c3x%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment-nz_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:%c3%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment_empty_pchar - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz/0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ%20!$&'()*+,;=:@%20"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz/%61%20%23"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz/%c3x%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_segment_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz/%c3%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_with_query_and_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz?q#f"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_with_segment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz/a"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/path-rootless_with_segments - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo:nz//segment//segment/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/port_0 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:0"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/port_1 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:1"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/port_65535 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:65535"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/port_65536 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:65536"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/port_8080 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:8080"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/port_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com:"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/port_empty_reg-name_empty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://:"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?baz=quux"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~%20!$&'()*+,=;:@?"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_extra - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?/?"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_pchar_extra - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?:@"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?%61%20%23"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_pct-encoded_invalid_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?%c3x%96"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?%c3%96%c3"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_sub-delim_semicolon - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?;"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_sub-delims - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?!$&'()*+,="} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/query_unusual_key_value_structure - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://example.com?a=b&c&&=1&=="} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/scheme_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"foo0123456789azAZ+-.://example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/scheme_ftp - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"ftp://example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_exhaust - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~!$&'()*+,;=::@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_extra - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://:@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_multiple_colons - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://:::@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_name - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://user@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_name_password - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://user:password@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_pct-encoded_ascii - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://%61%20%23@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_pct-encoded_invalid-utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://%c3x%963@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_pct-encoded_utf8 - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://%c3%963@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_reserved_hash_parses_as_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://#@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_reserved_questionmark_parses_as_query - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://?@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_reserved_slash_parses_as_path-abempty - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https:///@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_sub-delims - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://!$&'()*+,;=@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false - - valid/userinfo_unreserved - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUri]:{val:"https://0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~@example.com"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri" - # message: "" - # for_key: false -library/is_uri_ref: - - invalid/authority_path-abempty_segment_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host/\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/bad_relative-part - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:":"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/leading_space - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:" ./foo"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-abempty_query_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host?^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-abempty_query_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host?\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-abempty_query_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host?%"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-abempty_segment_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host/%x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/foo/\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_query_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/?^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_query_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/?\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_query_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/?%"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_segment-nz_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_segment-nz_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_segment-nz_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/%x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_segment_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/nz/^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_segment_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/nz/\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-absolute_segment_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/nz/%x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-empty_query_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"?^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-empty_query_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"?\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-empty_query_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"?%"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"./foo/\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_query_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:".?^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_query_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:".?\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_query_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:".?%"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_segment-bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"./\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_segment-nz_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_segment-nz_bad_colon - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:":"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_segment-nz_bad_control_character - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"\x1f"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_segment-nz_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"%x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_segment_bad_caret - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"./^"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/path-noscheme_segment_bad_pct-encoded - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"./%x"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/space - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:" "} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/trailing_space - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"./foo "} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - invalid/uri_with_bad_scheme - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"1foo://example.com"} - # want: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # got: valid - - valid/authority_path-abempty_with_segment_query_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host/foo?baz=quux#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/extreme - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//userinfo0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~!$&'()*+,;=::@host!$&'()*+,;=._~0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ:0123456789/path0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ%20!$&'()*+,;=:@%20//foo/?query0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~%20!$&'()*+,=;:@?#fragment0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~%20!$&'()*+,=;:@?/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-abempty_exhaust_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host#0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~%20!$&'()*+,=;:@?/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-abempty_with_fragment/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-abempty_with_fragment/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"//host/foo/bar#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-absolute_exhaust_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/#0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~%20!$&'()*+,=;:@?/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-absolute_with_fragment/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-absolute_with_fragment/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"/foo/bar#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-empty_exhaust_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"#0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~%20!$&'()*+,=;:@?/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-noscheme_exhaust_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:".#0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-._~%20!$&'()*+,=;:@?/"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-noscheme_with_fragment/a - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:".#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-noscheme_with_fragment/b - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"./foo/bar#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false - - valid/path-noscheme_with_segment_query_fragment - # input: [type.googleapis.com/buf.validate.conformance.cases.IsUriRef]:{val:"./foo/bar?baz=quux#frag"} - # want: valid - # got: validation error (1 violation) - # 1. constraint_id: "library.is_uri_ref" - # message: "" - # for_key: false -standard_constraints/required: +standard_rules/required: - proto2/scalar/optional/unset # input: [type.googleapis.com/buf.validate.conformance.cases.RequiredProto2ScalarOptional]:{} # want: validation error (1 violation) - # 1. constraint_id: "required" + # 1. rule_id: "required" # field: "val" elements:{field_number:1 field_name:"val" field_type:TYPE_STRING} # rule: "required" elements:{field_number:25 field_name:"required" field_type:TYPE_BOOL} # got: valid - proto2/scalar/optional_with_default/unset # input: [type.googleapis.com/buf.validate.conformance.cases.RequiredProto2ScalarOptionalDefault]:{} # want: validation error (1 violation) - # 1. constraint_id: "required" + # 1. rule_id: "required" # field: "val" elements:{field_number:1 field_name:"val" field_type:TYPE_STRING} # rule: "required" elements:{field_number:25 field_name:"required" field_type:TYPE_BOOL} # got: valid diff --git a/tests/conformance/runner.py b/tests/conformance/runner.py index 7bd6a4c1..353fd7f4 100644 --- a/tests/conformance/runner.py +++ b/tests/conformance/runner.py @@ -54,7 +54,7 @@ wkt_timestamp_pb2, # noqa: F401 wkt_wrappers_pb2, # noqa: F401 ) -from buf.validate.conformance.cases.custom_constraints import custom_constraints_pb2 # noqa: F401 +from buf.validate.conformance.cases.custom_rules import custom_rules_pb2 # noqa: F401 from buf.validate.conformance.harness import harness_pb2 diff --git a/tests/validate_test.py b/tests/validate_test.py index e95aa9e8..e8c16e51 100644 --- a/tests/validate_test.py +++ b/tests/validate_test.py @@ -24,7 +24,7 @@ def test_ninf(self): msg.val = float("-inf") violations = protovalidate.collect_violations(msg) self.assertEqual(len(violations), 1) - self.assertEqual(violations[0].proto.constraint_id, "double.finite") + self.assertEqual(violations[0].proto.rule_id, "double.finite") self.assertEqual(violations[0].field_value, msg.val) self.assertEqual(violations[0].rule_value, True)