From 1f62639d5cf0ece82221e0b43068e5ab42e9fe09 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 11 Jun 2025 16:23:33 -0400 Subject: [PATCH 1/7] Fix flags --- protovalidate/internal/extra_func.py | 82 +++++++++++++++++++++++++++- tests/matches_test.py | 50 +++++++++++++++++ 2 files changed, 131 insertions(+), 1 deletion(-) create mode 100644 tests/matches_test.py diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index ae2b02c0..c0d4912c 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -14,6 +14,9 @@ import math import re +import sys +from functools import reduce +import operator import typing from urllib import parse as urlparse @@ -1553,13 +1556,90 @@ def __peek(self, char: str) -> bool: return self._index < len(self._string) and self._string[self._index] == char +# Patterns that are supported in Python's re package and not in re2. +# RE2: https://github.com/google/re2/wiki/syntax +invalid_patterns = [ + r"\\[1-9]", # backreference + r"\\k<\w+>", # backreference + r"\(\?\=", # lookahead + r"\(\?\!", # negative lookahead + r"\(\?\<\=", # lookbehind + r"\(\?\<\!", # negative lookbehind + r"\\c[A-Z]", # control character + r"\\u[0-9a-fA-F]{4}", # UTF-16 code-unit + r"\\0(?!\d)", # NUL + r"\[\\b.*\]", # Backspace eg: [\b] +] + +flag_pattern = re.compile(r"^\(\?(?P[ims\-]+)\)"); + +flag_mapping = { + "a": re.A, + "i": re.I, + "l": re.L, + "m": re.M, +} + +def flags_from_letters(letters: str) -> int: + return reduce(operator.or_, (flag_mapping[c] for c in letters if c in flag_mapping), 0) + +def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: + if not isinstance(text, celtypes.StringType): + msg = "invalid argument for text, expected string" + raise celpy.CELEvalError(msg) + if not isinstance(pattern, celtypes.StringType): + msg = "invalid argument for pattern, expected string" + raise celpy.CELEvalError(msg) + + for invalid_pattern in invalid_patterns: + r = re.search(invalid_pattern, pattern) + if r is not None: + msg = f"error evaluating pattern {pattern}, invalid RE2 syntax" + raise celpy.CELEvalError(msg) + # CEL uses RE2 syntax which is a subset of Python re except for + # the flags and the ability to change the flags mid sequence. + # + # The conformance tests use flags at the very beginning of the sequence, which + # is likely the most common place where this rare feature will be used. + # + # Instead of importing an RE2 engine to be able to support this niche, we + # can instead just check for the flags at the very beginning and apply them. + # + # Unsupported flags and flags mid sequence will fail to compile the regex. + # + # Users can choose to override this function and provide an RE2 engine if they really need to. + flags = "" + flag_matches = re.match(flag_pattern, pattern) + pattern_str = pattern + if flag_matches is not None: + ms = flag_matches.groupdict() + flagsies = ms["flags"] + for fl in flagsies: + if fl == "-": + continue + flags += fl + + pattern_str = pattern[len(flag_matches[0]):] + flags_enums = flags_from_letters(flags) + + expresh = re.compile(pattern_str, flags=flags_enums) + + try: + m = re.search(expresh, text) + except re.error as ex: + return celpy.CELEvalError("match error", ex.__class__, ex.args) + + return celtypes.BoolType(m is not None) + + def make_extra_funcs(locale: str) -> dict[str, celpy.CELFunction]: - # TODO(#257): Fix types and add tests for StringFormat. # For now, ignoring the type. string_fmt = string_format.StringFormat(locale) # type: ignore return { # Missing standard functions "format": string_fmt.format, + # Overridden standard functions + "matches": cel_matches, # protovalidate specific functions "getField": cel_get_field, "isNan": cel_is_nan, diff --git a/tests/matches_test.py b/tests/matches_test.py new file mode 100644 index 00000000..a4274499 --- /dev/null +++ b/tests/matches_test.py @@ -0,0 +1,50 @@ +# Copyright 2023-2025 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import unittest + +import celpy +from celpy import celtypes + +from protovalidate.internal import extra_func + +invalid_patterns = [ + r"\1", + r"\k", + r"Jack(?=Sprat)", + "Jack(?!Sprat)", + "(?<=Sprat)Jack", + "(? None: + result = extra_func.cel_matches(celtypes.StringType("!@#$%^&*()"), celtypes.StringType("(?i)^[a-z0-9]+$")) + self.assertFalse(result) + + From 3bb71cb3ecc089963cdfa0c56d8fc24ac930f0e2 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Wed, 11 Jun 2025 16:39:40 -0400 Subject: [PATCH 2/7] Tests --- protovalidate/internal/extra_func.py | 63 +++++++++++++--------------- tests/matches_test.py | 2 - 2 files changed, 30 insertions(+), 35 deletions(-) diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index c0d4912c..14f9b18c 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -13,11 +13,10 @@ # limitations under the License. import math -import re -import sys -from functools import reduce import operator +import re import typing +from functools import reduce from urllib import parse as urlparse import celpy @@ -1571,17 +1570,14 @@ def __peek(self, char: str) -> bool: r"\[\\b.*\]", # Backspace eg: [\b] ] -flag_pattern = re.compile(r"^\(\?(?P[ims\-]+)\)"); - +flag_pattern = re.compile(r"^\(\?(?P[ims\-]+)\)") flag_mapping = { - "a": re.A, - "i": re.I, - "l": re.L, - "m": re.M, + "a": re.A, + "i": re.I, + "l": re.L, + "m": re.M, } -def flags_from_letters(letters: str) -> int: - return reduce(operator.or_, (flag_mapping[c] for c in letters if c in flag_mapping), 0) def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: if not isinstance(text, celtypes.StringType): @@ -1591,41 +1587,42 @@ def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: msg = "invalid argument for pattern, expected string" raise celpy.CELEvalError(msg) + # Simulate re2 by failing on any patterns not compatible with re2 syntax for invalid_pattern in invalid_patterns: r = re.search(invalid_pattern, pattern) if r is not None: msg = f"error evaluating pattern {pattern}, invalid RE2 syntax" raise celpy.CELEvalError(msg) - # CEL uses RE2 syntax which is a subset of Python re except for - # the flags and the ability to change the flags mid sequence. - # - # The conformance tests use flags at the very beginning of the sequence, which - # is likely the most common place where this rare feature will be used. - # - # Instead of importing an RE2 engine to be able to support this niche, we - # can instead just check for the flags at the very beginning and apply them. - # - # Unsupported flags and flags mid sequence will fail to compile the regex. - # - # Users can choose to override this function and provide an RE2 engine if they really need to. + + # CEL uses RE2 syntax which is a subset of Python re except for + # the flags and the ability to change the flags mid sequence. + # + # The conformance tests use flags at the very beginning of the sequence, which + # is likely the most common place where this rare feature will be used. + # + # Instead of importing an RE2 engine to be able to support this niche, we + # can instead just check for the flags at the very beginning and apply them. + # + # Unsupported flags and flags mid sequence will fail to compile the regex. + # + # Users can choose to override this function and provide an RE2 engine if they really need to. flags = "" flag_matches = re.match(flag_pattern, pattern) - pattern_str = pattern if flag_matches is not None: - ms = flag_matches.groupdict() - flagsies = ms["flags"] - for fl in flagsies: + flag_group = flag_matches.groupdict()["flags"] + for fl in flag_group: + # Flag removal, don't include it in the output if fl == "-": continue flags += fl - - pattern_str = pattern[len(flag_matches[0]):] - flags_enums = flags_from_letters(flags) - - expresh = re.compile(pattern_str, flags=flags_enums) + pattern_str = pattern[len(flag_matches[0]) :] + flags_enums = reduce(operator.or_, (flag_mapping[c] for c in flags if c in flag_mapping), 0) + exp = re.compile(pattern_str, flags=flags_enums) + else: + exp = re.compile(pattern) try: - m = re.search(expresh, text) + m = re.search(exp, text) except re.error as ex: return celpy.CELEvalError("match error", ex.__class__, ex.args) diff --git a/tests/matches_test.py b/tests/matches_test.py index a4274499..9708b741 100644 --- a/tests/matches_test.py +++ b/tests/matches_test.py @@ -46,5 +46,3 @@ def test_invalid_re2_syntax(self): def test_flags(self) -> None: result = extra_func.cel_matches(celtypes.StringType("!@#$%^&*()"), celtypes.StringType("(?i)^[a-z0-9]+$")) self.assertFalse(result) - - From 11717408e3490e7c594f5c31343abf997a9718a7 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 12 Jun 2025 11:17:52 -0400 Subject: [PATCH 3/7] Simulate re2 --- protovalidate/internal/extra_func.py | 77 +------------------ protovalidate/internal/matches.py | 106 +++++++++++++++++++++++++++ tests/matches_test.py | 10 ++- 3 files changed, 115 insertions(+), 78 deletions(-) create mode 100644 protovalidate/internal/matches.py diff --git a/protovalidate/internal/extra_func.py b/protovalidate/internal/extra_func.py index 14f9b18c..442cdb77 100644 --- a/protovalidate/internal/extra_func.py +++ b/protovalidate/internal/extra_func.py @@ -13,16 +13,15 @@ # limitations under the License. import math -import operator import re import typing -from functools import reduce from urllib import parse as urlparse import celpy from celpy import celtypes from protovalidate.internal import string_format +from protovalidate.internal.matches import cel_matches from protovalidate.internal.rules import MessageType, field_to_cel # See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address @@ -1555,80 +1554,6 @@ def __peek(self, char: str) -> bool: return self._index < len(self._string) and self._string[self._index] == char -# Patterns that are supported in Python's re package and not in re2. -# RE2: https://github.com/google/re2/wiki/syntax -invalid_patterns = [ - r"\\[1-9]", # backreference - r"\\k<\w+>", # backreference - r"\(\?\=", # lookahead - r"\(\?\!", # negative lookahead - r"\(\?\<\=", # lookbehind - r"\(\?\<\!", # negative lookbehind - r"\\c[A-Z]", # control character - r"\\u[0-9a-fA-F]{4}", # UTF-16 code-unit - r"\\0(?!\d)", # NUL - r"\[\\b.*\]", # Backspace eg: [\b] -] - -flag_pattern = re.compile(r"^\(\?(?P[ims\-]+)\)") -flag_mapping = { - "a": re.A, - "i": re.I, - "l": re.L, - "m": re.M, -} - - -def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: - if not isinstance(text, celtypes.StringType): - msg = "invalid argument for text, expected string" - raise celpy.CELEvalError(msg) - if not isinstance(pattern, celtypes.StringType): - msg = "invalid argument for pattern, expected string" - raise celpy.CELEvalError(msg) - - # Simulate re2 by failing on any patterns not compatible with re2 syntax - for invalid_pattern in invalid_patterns: - r = re.search(invalid_pattern, pattern) - if r is not None: - msg = f"error evaluating pattern {pattern}, invalid RE2 syntax" - raise celpy.CELEvalError(msg) - - # CEL uses RE2 syntax which is a subset of Python re except for - # the flags and the ability to change the flags mid sequence. - # - # The conformance tests use flags at the very beginning of the sequence, which - # is likely the most common place where this rare feature will be used. - # - # Instead of importing an RE2 engine to be able to support this niche, we - # can instead just check for the flags at the very beginning and apply them. - # - # Unsupported flags and flags mid sequence will fail to compile the regex. - # - # Users can choose to override this function and provide an RE2 engine if they really need to. - flags = "" - flag_matches = re.match(flag_pattern, pattern) - if flag_matches is not None: - flag_group = flag_matches.groupdict()["flags"] - for fl in flag_group: - # Flag removal, don't include it in the output - if fl == "-": - continue - flags += fl - pattern_str = pattern[len(flag_matches[0]) :] - flags_enums = reduce(operator.or_, (flag_mapping[c] for c in flags if c in flag_mapping), 0) - exp = re.compile(pattern_str, flags=flags_enums) - else: - exp = re.compile(pattern) - - try: - m = re.search(exp, text) - except re.error as ex: - return celpy.CELEvalError("match error", ex.__class__, ex.args) - - return celtypes.BoolType(m is not None) - - def make_extra_funcs(locale: str) -> dict[str, celpy.CELFunction]: # For now, ignoring the type. string_fmt = string_format.StringFormat(locale) # type: ignore diff --git a/protovalidate/internal/matches.py b/protovalidate/internal/matches.py new file mode 100644 index 00000000..54ab0e07 --- /dev/null +++ b/protovalidate/internal/matches.py @@ -0,0 +1,106 @@ +# Copyright 2023-2025 Buf Technologies, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import operator +import re +from functools import reduce + +import celpy +from celpy import celtypes + +# Patterns that are supported in Python's re package and not in re2. +# RE2: https://github.com/google/re2/wiki/syntax +invalid_patterns = [ + r"\\[1-9]", # backreference + r"\\k<\w+>", # backreference + r"\(\?\=", # lookahead + r"\(\?\!", # negative lookahead + r"\(\?\<\=", # lookbehind + r"\(\?\<\!", # negative lookbehind + r"\\c[A-Z]", # control character + r"\\u[0-9a-fA-F]{4}", # UTF-16 code-unit + r"\\0(?!\d)", # NUL + r"\[\\b.*\]", # Backspace eg: [\b] +] + +# Regex for searching a regex pattern for flags. +flag_pattern = re.compile(r"^\(\?(?P[ims\-]+)\)") + +# See https://docs.python.org/3/library/re.html#flags +flag_mapping = { + "a": re.A, + "i": re.I, + "L": re.L, + "m": re.M, + "s": re.S, + "u": re.U, + "x": re.X, +} + + +def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: + """Return True if the given pattern matches text. False otherwise. + + CEL uses RE2 syntax which diverges from Python re in various ways. Ideally, we + would use the google-re2 package, which is an extra dep in celpy, but at press + time it does not provide a pre-built binary for the latest version of Python (3.13) + which means those using this version will run into many issues. + + Instead of foisting this issue on users, we instead mimic re2 syntax by failing + to compile the regex for patterns not compatible with re2. + + If users really want a pure re2 engine, they can provide their own via a config + parameter when creating a validator. + """ + if not isinstance(text, celtypes.StringType): + msg = "invalid argument for text, expected string" + raise celpy.CELEvalError(msg) + if not isinstance(pattern, celtypes.StringType): + msg = "invalid argument for pattern, expected string" + raise celpy.CELEvalError(msg) + + # Simulate re2 by failing on any patterns not compatible with re2 syntax + for invalid_pattern in invalid_patterns: + r = re.search(invalid_pattern, pattern) + if r is not None: + msg = f"error evaluating pattern {pattern}, invalid RE2 syntax" + raise celpy.CELEvalError(msg) + # The conformance tests use flags at the very beginning of the sequence, which + # is likely the most common place where this rare feature will be used. + # + # So we check for the flags at the very beginning and if present, apply them + # using Python re enums. + flags = "" + flag_matches = re.match(flag_pattern, pattern) + if flag_matches is not None: + flag_group = flag_matches.groupdict()["flags"] + for fl in flag_group: + # Flag removal, don't include it in the output + if fl == "-": + continue + flags += fl + # Grab the rest of the expression minus the flags + pattern_str = pattern[len(flag_matches[0]) :] + # Convert a string of flags (i.e. aiLm) into the actual re.A, re.I enums + flags_enums = reduce(operator.or_, (flag_mapping[c] for c in flags if c in flag_mapping), 0) + exp = re.compile(pattern_str, flags=flags_enums) + else: + exp = re.compile(pattern) + + try: + m = re.search(exp, text) + except re.error as ex: + return celpy.CELEvalError("match error", ex.__class__, ex.args) + + return celtypes.BoolType(m is not None) diff --git a/tests/matches_test.py b/tests/matches_test.py index 9708b741..87818cca 100644 --- a/tests/matches_test.py +++ b/tests/matches_test.py @@ -44,5 +44,11 @@ def test_invalid_re2_syntax(self): self.assertEqual(str(e), f"error evaluating pattern {cel_pattern}, invalid RE2 syntax") def test_flags(self) -> None: - result = extra_func.cel_matches(celtypes.StringType("!@#$%^&*()"), celtypes.StringType("(?i)^[a-z0-9]+$")) - self.assertFalse(result) + self.assertTrue(extra_func.cel_matches(celtypes.StringType("foobar"), celtypes.StringType("(?i:foo)(?-i:bar)"))) + self.assertTrue(extra_func.cel_matches(celtypes.StringType("FOObar"), celtypes.StringType("(?i:foo)(?-i:bar)"))) + self.assertFalse( + extra_func.cel_matches(celtypes.StringType("fooBAR"), celtypes.StringType("(?i:foo)(?-i:bar)")) + ) + self.assertFalse( + extra_func.cel_matches(celtypes.StringType("FOOBAR"), celtypes.StringType("(?i:foo)(?-i:bar)")) + ) From 63a2a594af5a42305aadc553acbfc8ee1d2a9ed9 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 12 Jun 2025 11:21:37 -0400 Subject: [PATCH 4/7] Remove comment for now --- protovalidate/internal/matches.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/protovalidate/internal/matches.py b/protovalidate/internal/matches.py index 54ab0e07..9c785597 100644 --- a/protovalidate/internal/matches.py +++ b/protovalidate/internal/matches.py @@ -59,9 +59,6 @@ def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: Instead of foisting this issue on users, we instead mimic re2 syntax by failing to compile the regex for patterns not compatible with re2. - - If users really want a pure re2 engine, they can provide their own via a config - parameter when creating a validator. """ if not isinstance(text, celtypes.StringType): msg = "invalid argument for text, expected string" From e92305041c9292652f724ca58a5d1000a10cabaf Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Thu, 12 Jun 2025 17:40:54 -0400 Subject: [PATCH 5/7] Validate `MessageOneofRule` (#320) https://github.com/bufbuild/protovalidate/pull/379. --- Makefile | 2 +- buf.yaml | 4 +- .../conformance/cases/messages_pb2.py | 34 +++++++++++--- .../conformance/cases/messages_pb2.pyi | 44 +++++++++++++++++++ protovalidate/internal/rules.py | 9 ++++ 5 files changed, 83 insertions(+), 10 deletions(-) diff --git a/Makefile b/Makefile index 30ae1c57..d4e613ed 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ ADD_LICENSE_HEADER := $(BIN)/license-header \ --copyright-holder "Buf Technologies, Inc." \ --year-range "2023-2025" # This version should be kept in sync with the version in buf.yaml -PROTOVALIDATE_VERSION ?= v0.13.0 +PROTOVALIDATE_VERSION ?= v0.13.1 # Version of the cel-spec that this implementation is conformant with # This should be kept in sync with the version in format_test.py CEL_SPEC_VERSION ?= v0.24.0 diff --git a/buf.yaml b/buf.yaml index 2acbae6d..608c755f 100644 --- a/buf.yaml +++ b/buf.yaml @@ -2,8 +2,8 @@ version: v2 modules: - path: proto deps: - - buf.build/bufbuild/protovalidate:v0.13.0 - - buf.build/bufbuild/protovalidate-testing:v0.13.0 + - buf.build/bufbuild/protovalidate:v0.13.1 + - buf.build/bufbuild/protovalidate-testing:v0.13.1 lint: use: - STANDARD diff --git a/gen/buf/validate/conformance/cases/messages_pb2.py b/gen/buf/validate/conformance/cases/messages_pb2.py index b1f30bc2..4074d6e7 100644 --- a/gen/buf/validate/conformance/cases/messages_pb2.py +++ b/gen/buf/validate/conformance/cases/messages_pb2.py @@ -40,7 +40,7 @@ from buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-buf/validate/conformance/cases/messages.proto\x12\x1e\x62uf.validate.conformance.cases\x1a\x38\x62uf/validate/conformance/cases/other_package/embed.proto\x1a\x1b\x62uf/validate/validate.proto\"l\n\x07TestMsg\x12 \n\x05\x63onst\x18\x01 \x01(\tB\n\xbaH\x07r\x05\n\x03\x66ooR\x05\x63onst\x12?\n\x06nested\x18\x02 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x06nested\"_\n\x0bMessageNone\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.MessageNone.NoneMsgR\x03val\x1a\t\n\x07NoneMsg\"3\n\x0fMessageDisabled\x12\x19\n\x03val\x18\x01 \x01(\x04\x42\x07\xbaH\x04\x32\x02 {R\x03val:\x05\xbaH\x02\x08\x01\"D\n\x07Message\x12\x39\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x03val\"\\\n\x13MessageCrossPackage\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.other_package.EmbedR\x03val\"P\n\x0bMessageSkip\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xd8\x01\x03R\x03val\"T\n\x0fMessageRequired\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01R\x03val\"l\n\x1aMessageRequiredButOptional\x12\x46\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03val\x88\x01\x01\x42\x06\n\x04_val\"i\n\x14MessageRequiredOneof\x12\x43\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03valB\x0c\n\x03one\x12\x05\xbaH\x02\x08\x01\"\x15\n\x13MessageWith3dInside\"g\n\x17MessageOneofSingleField\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x10\xbaH\r\"\x0b\n\tstr_field\"v\n\x1aMessageOneofMultipleFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1c\xbaH\x19\"\x17\n\tstr_field\n\nbool_field\"\x80\x01\n\"MessageOneofMultipleFieldsRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1e\xbaH\x1b\"\x19\n\tstr_field\n\nbool_field\x10\x01\"G\n\x1cMessageOneofUnknownFieldName\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField:\n\xbaH\x07\"\x05\n\x03xxxB\xcf\x01\n\"com.buf.validate.conformance.casesB\rMessagesProtoP\x01\xa2\x02\x04\x42VCC\xaa\x02\x1e\x42uf.Validate.Conformance.Cases\xca\x02\x1e\x42uf\\Validate\\Conformance\\Cases\xe2\x02*Buf\\Validate\\Conformance\\Cases\\GPBMetadata\xea\x02!Buf::Validate::Conformance::Casesb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-buf/validate/conformance/cases/messages.proto\x12\x1e\x62uf.validate.conformance.cases\x1a\x38\x62uf/validate/conformance/cases/other_package/embed.proto\x1a\x1b\x62uf/validate/validate.proto\"l\n\x07TestMsg\x12 \n\x05\x63onst\x18\x01 \x01(\tB\n\xbaH\x07r\x05\n\x03\x66ooR\x05\x63onst\x12?\n\x06nested\x18\x02 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x06nested\"_\n\x0bMessageNone\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.MessageNone.NoneMsgR\x03val\x1a\t\n\x07NoneMsg\"3\n\x0fMessageDisabled\x12\x19\n\x03val\x18\x01 \x01(\x04\x42\x07\xbaH\x04\x32\x02 {R\x03val:\x05\xbaH\x02\x08\x01\"D\n\x07Message\x12\x39\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x03val\"\\\n\x13MessageCrossPackage\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.other_package.EmbedR\x03val\"P\n\x0bMessageSkip\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xd8\x01\x03R\x03val\"T\n\x0fMessageRequired\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01R\x03val\"l\n\x1aMessageRequiredButOptional\x12\x46\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03val\x88\x01\x01\x42\x06\n\x04_val\"i\n\x14MessageRequiredOneof\x12\x43\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03valB\x0c\n\x03one\x12\x05\xbaH\x02\x08\x01\"\x15\n\x13MessageWith3dInside\"g\n\x17MessageOneofSingleField\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x10\xbaH\r\"\x0b\n\tstr_field\"q\n\x1fMessageOneofSingleFieldRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x12\xbaH\x0f\"\r\n\tstr_field\x10\x01\"v\n\x1aMessageOneofMultipleFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1c\xbaH\x19\"\x17\n\tstr_field\n\nbool_field\"\x80\x01\n\"MessageOneofMultipleFieldsRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1e\xbaH\x1b\"\x19\n\tstr_field\n\nbool_field\x10\x01\"\xb5\x01\n MessageOneofMultipleSharedFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField\x12\x1b\n\tint_field\x18\x03 \x01(\x05R\x08intField:8\xbaH5\"\x19\n\tstr_field\n\nbool_field\x10\x01\"\x18\n\tstr_field\n\tint_field\x10\x01\"G\n\x1cMessageOneofUnknownFieldName\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField:\n\xbaH\x07\"\x05\n\x03xxx\"\x81\x01\n\x1aMessageOneofDuplicateField\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\'\xbaH$\"\"\n\tstr_field\n\nbool_field\n\tstr_field\"[\n\x16MessageOneofZeroFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x05\xbaH\x02\"\x00\"h\n\x19MessageOneofUnsatisfiable\x12\x0c\n\x01\x61\x18\x01 \x01(\x08R\x01\x61\x12\x0c\n\x01\x62\x18\x02 \x01(\x08R\x01\x62\x12\x0c\n\x01\x63\x18\x03 \x01(\x08R\x01\x63:!\xbaH\x1e\"\x08\n\x01\x61\n\x01\x62\x10\x01\"\x08\n\x01\x62\n\x01\x63\x10\x01\"\x08\n\x01\x61\n\x01\x63\x10\x01\x42\xcf\x01\n\"com.buf.validate.conformance.casesB\rMessagesProtoP\x01\xa2\x02\x04\x42VCC\xaa\x02\x1e\x42uf.Validate.Conformance.Cases\xca\x02\x1e\x42uf\\Validate\\Conformance\\Cases\xe2\x02*Buf\\Validate\\Conformance\\Cases\\GPBMetadata\xea\x02!Buf::Validate::Conformance::Casesb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -66,12 +66,22 @@ _globals['_MESSAGEREQUIREDONEOF'].fields_by_name['val']._serialized_options = b'\272H\003\310\001\001' _globals['_MESSAGEONEOFSINGLEFIELD']._loaded_options = None _globals['_MESSAGEONEOFSINGLEFIELD']._serialized_options = b'\272H\r\"\013\n\tstr_field' + _globals['_MESSAGEONEOFSINGLEFIELDREQUIRED']._loaded_options = None + _globals['_MESSAGEONEOFSINGLEFIELDREQUIRED']._serialized_options = b'\272H\017\"\r\n\tstr_field\020\001' _globals['_MESSAGEONEOFMULTIPLEFIELDS']._loaded_options = None _globals['_MESSAGEONEOFMULTIPLEFIELDS']._serialized_options = b'\272H\031\"\027\n\tstr_field\n\nbool_field' _globals['_MESSAGEONEOFMULTIPLEFIELDSREQUIRED']._loaded_options = None _globals['_MESSAGEONEOFMULTIPLEFIELDSREQUIRED']._serialized_options = b'\272H\033\"\031\n\tstr_field\n\nbool_field\020\001' + _globals['_MESSAGEONEOFMULTIPLESHAREDFIELDS']._loaded_options = None + _globals['_MESSAGEONEOFMULTIPLESHAREDFIELDS']._serialized_options = b'\272H5\"\031\n\tstr_field\n\nbool_field\020\001\"\030\n\tstr_field\n\tint_field\020\001' _globals['_MESSAGEONEOFUNKNOWNFIELDNAME']._loaded_options = None _globals['_MESSAGEONEOFUNKNOWNFIELDNAME']._serialized_options = b'\272H\007\"\005\n\003xxx' + _globals['_MESSAGEONEOFDUPLICATEFIELD']._loaded_options = None + _globals['_MESSAGEONEOFDUPLICATEFIELD']._serialized_options = b'\272H$\"\"\n\tstr_field\n\nbool_field\n\tstr_field' + _globals['_MESSAGEONEOFZEROFIELDS']._loaded_options = None + _globals['_MESSAGEONEOFZEROFIELDS']._serialized_options = b'\272H\002\"\000' + _globals['_MESSAGEONEOFUNSATISFIABLE']._loaded_options = None + _globals['_MESSAGEONEOFUNSATISFIABLE']._serialized_options = b'\272H\036\"\010\n\001a\n\001b\020\001\"\010\n\001b\n\001c\020\001\"\010\n\001a\n\001c\020\001' _globals['_TESTMSG']._serialized_start=168 _globals['_TESTMSG']._serialized_end=276 _globals['_MESSAGENONE']._serialized_start=278 @@ -96,10 +106,20 @@ _globals['_MESSAGEWITH3DINSIDE']._serialized_end=998 _globals['_MESSAGEONEOFSINGLEFIELD']._serialized_start=1000 _globals['_MESSAGEONEOFSINGLEFIELD']._serialized_end=1103 - _globals['_MESSAGEONEOFMULTIPLEFIELDS']._serialized_start=1105 - _globals['_MESSAGEONEOFMULTIPLEFIELDS']._serialized_end=1223 - _globals['_MESSAGEONEOFMULTIPLEFIELDSREQUIRED']._serialized_start=1226 - _globals['_MESSAGEONEOFMULTIPLEFIELDSREQUIRED']._serialized_end=1354 - _globals['_MESSAGEONEOFUNKNOWNFIELDNAME']._serialized_start=1356 - _globals['_MESSAGEONEOFUNKNOWNFIELDNAME']._serialized_end=1427 + _globals['_MESSAGEONEOFSINGLEFIELDREQUIRED']._serialized_start=1105 + _globals['_MESSAGEONEOFSINGLEFIELDREQUIRED']._serialized_end=1218 + _globals['_MESSAGEONEOFMULTIPLEFIELDS']._serialized_start=1220 + _globals['_MESSAGEONEOFMULTIPLEFIELDS']._serialized_end=1338 + _globals['_MESSAGEONEOFMULTIPLEFIELDSREQUIRED']._serialized_start=1341 + _globals['_MESSAGEONEOFMULTIPLEFIELDSREQUIRED']._serialized_end=1469 + _globals['_MESSAGEONEOFMULTIPLESHAREDFIELDS']._serialized_start=1472 + _globals['_MESSAGEONEOFMULTIPLESHAREDFIELDS']._serialized_end=1653 + _globals['_MESSAGEONEOFUNKNOWNFIELDNAME']._serialized_start=1655 + _globals['_MESSAGEONEOFUNKNOWNFIELDNAME']._serialized_end=1726 + _globals['_MESSAGEONEOFDUPLICATEFIELD']._serialized_start=1729 + _globals['_MESSAGEONEOFDUPLICATEFIELD']._serialized_end=1858 + _globals['_MESSAGEONEOFZEROFIELDS']._serialized_start=1860 + _globals['_MESSAGEONEOFZEROFIELDS']._serialized_end=1951 + _globals['_MESSAGEONEOFUNSATISFIABLE']._serialized_start=1953 + _globals['_MESSAGEONEOFUNSATISFIABLE']._serialized_end=2057 # @@protoc_insertion_point(module_scope) diff --git a/gen/buf/validate/conformance/cases/messages_pb2.pyi b/gen/buf/validate/conformance/cases/messages_pb2.pyi index 3f48780c..618c724b 100644 --- a/gen/buf/validate/conformance/cases/messages_pb2.pyi +++ b/gen/buf/validate/conformance/cases/messages_pb2.pyi @@ -92,6 +92,14 @@ class MessageOneofSingleField(_message.Message): bool_field: bool def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ...) -> None: ... +class MessageOneofSingleFieldRequired(_message.Message): + __slots__ = ("str_field", "bool_field") + STR_FIELD_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_FIELD_NUMBER: _ClassVar[int] + str_field: str + bool_field: bool + def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ...) -> None: ... + class MessageOneofMultipleFields(_message.Message): __slots__ = ("str_field", "bool_field") STR_FIELD_FIELD_NUMBER: _ClassVar[int] @@ -108,8 +116,44 @@ class MessageOneofMultipleFieldsRequired(_message.Message): bool_field: bool def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ...) -> None: ... +class MessageOneofMultipleSharedFields(_message.Message): + __slots__ = ("str_field", "bool_field", "int_field") + STR_FIELD_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_FIELD_NUMBER: _ClassVar[int] + INT_FIELD_FIELD_NUMBER: _ClassVar[int] + str_field: str + bool_field: bool + int_field: int + def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ..., int_field: _Optional[int] = ...) -> None: ... + class MessageOneofUnknownFieldName(_message.Message): __slots__ = ("str_field",) STR_FIELD_FIELD_NUMBER: _ClassVar[int] str_field: str def __init__(self, str_field: _Optional[str] = ...) -> None: ... + +class MessageOneofDuplicateField(_message.Message): + __slots__ = ("str_field", "bool_field") + STR_FIELD_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_FIELD_NUMBER: _ClassVar[int] + str_field: str + bool_field: bool + def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ...) -> None: ... + +class MessageOneofZeroFields(_message.Message): + __slots__ = ("str_field", "bool_field") + STR_FIELD_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_FIELD_NUMBER: _ClassVar[int] + str_field: str + bool_field: bool + def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ...) -> None: ... + +class MessageOneofUnsatisfiable(_message.Message): + __slots__ = ("a", "b", "c") + A_FIELD_NUMBER: _ClassVar[int] + B_FIELD_NUMBER: _ClassVar[int] + C_FIELD_NUMBER: _ClassVar[int] + a: bool + b: bool + c: bool + def __init__(self, a: bool = ..., b: bool = ..., c: bool = ...) -> None: ... diff --git a/protovalidate/internal/rules.py b/protovalidate/internal/rules.py index 015d281e..55d76408 100644 --- a/protovalidate/internal/rules.py +++ b/protovalidate/internal/rules.py @@ -454,9 +454,18 @@ def add_oneof( rule: validate_pb2.MessageOneofRule, ): fields = [] + seen = set() + if len(rule.fields) == 0: + msg = f"at least one field must be specified in oneof rule for the message {self._desc.full_name}" + raise CompilationError(msg) + for name in rule.fields: if name in self._desc.fields_by_name: + if name in seen: + msg = f"duplicate {name} in oneof rule for the message {self._desc.full_name}" + raise CompilationError(msg) fields.append(self._desc.fields_by_name[name]) + seen.add(name) else: msg = f'field "{name}" not found in message {self._desc.full_name}' raise CompilationError(msg) From 9fc2b6f93ff9e9c9e63c56cb3318646802d55705 Mon Sep 17 00:00:00 2001 From: Sri Krishna <93153132+srikrsna-buf@users.noreply.github.com> Date: Fri, 13 Jun 2025 21:30:28 +0530 Subject: [PATCH 6/7] Add implicit `IGNORE_IF_UNPOPULATED` for `MessageOneofRule` fields (#321) https://github.com/bufbuild/protovalidate/pull/382 --------- Signed-off-by: Sri Krishna --- Makefile | 2 +- buf.lock | 8 +++---- buf.yaml | 4 ++-- .../conformance/cases/messages_pb2.py | 20 +++++++++++++++- .../conformance/cases/messages_pb2.pyi | 24 +++++++++++++++++++ protovalidate/internal/rules.py | 8 +++++++ 6 files changed, 58 insertions(+), 8 deletions(-) diff --git a/Makefile b/Makefile index d4e613ed..5eae2af7 100644 --- a/Makefile +++ b/Makefile @@ -17,7 +17,7 @@ ADD_LICENSE_HEADER := $(BIN)/license-header \ --copyright-holder "Buf Technologies, Inc." \ --year-range "2023-2025" # This version should be kept in sync with the version in buf.yaml -PROTOVALIDATE_VERSION ?= v0.13.1 +PROTOVALIDATE_VERSION ?= v0.13.3 # Version of the cel-spec that this implementation is conformant with # This should be kept in sync with the version in format_test.py CEL_SPEC_VERSION ?= v0.24.0 diff --git a/buf.lock b/buf.lock index 588649e3..1abc9fc8 100644 --- a/buf.lock +++ b/buf.lock @@ -2,8 +2,8 @@ version: v2 deps: - name: buf.build/bufbuild/protovalidate - commit: c6451e2c05a64a659a0835011a72516c - digest: b5:826b8be86c1bc691afb751fb33c513cb90a9b9d03699b366a7812737e72fe06132d13ca4b2f5fdb9c5784594b0f65b20b850003349a556e8f020b84ce6787bd1 + commit: 9f2d3c737feb481a83375159c0733275 + digest: b5:19d3b83f7df2d284ff5935f4622d7f27e7464a93c210edb536e92a52bcc69b2a18da1312e96b5461601eba7b3764d5e90321bd62e6966870e7dbc2e4dedd98d6 - name: buf.build/bufbuild/protovalidate-testing - commit: 33dd956ff33d4a4085b2fa05cd7a14b8 - digest: b5:d142307939a7b8486fe7698e7d244c7e9c4a9c7ef812c97b46e8a7cc362f9c666f94f84dd66ad72fdb272ec23ed0cc415deed365318efdc9b24b92ef4e9ec415 + commit: 64cb206b26e840bcaacabae7b19bdf33 + digest: b5:f0abbad11668a70061ec8870c8c6b71f56e033ff97e0c22264648504e9a9c9f636304a01e27485eeacf7256f9bf25abc48503836948609f537a0fbc136be839b diff --git a/buf.yaml b/buf.yaml index 608c755f..9f4a0162 100644 --- a/buf.yaml +++ b/buf.yaml @@ -2,8 +2,8 @@ version: v2 modules: - path: proto deps: - - buf.build/bufbuild/protovalidate:v0.13.1 - - buf.build/bufbuild/protovalidate-testing:v0.13.1 + - buf.build/bufbuild/protovalidate:v0.13.3 + - buf.build/bufbuild/protovalidate-testing:v0.13.3 lint: use: - STANDARD diff --git a/gen/buf/validate/conformance/cases/messages_pb2.py b/gen/buf/validate/conformance/cases/messages_pb2.py index 4074d6e7..b54ad283 100644 --- a/gen/buf/validate/conformance/cases/messages_pb2.py +++ b/gen/buf/validate/conformance/cases/messages_pb2.py @@ -40,7 +40,7 @@ from buf.validate import validate_pb2 as buf_dot_validate_dot_validate__pb2 -DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-buf/validate/conformance/cases/messages.proto\x12\x1e\x62uf.validate.conformance.cases\x1a\x38\x62uf/validate/conformance/cases/other_package/embed.proto\x1a\x1b\x62uf/validate/validate.proto\"l\n\x07TestMsg\x12 \n\x05\x63onst\x18\x01 \x01(\tB\n\xbaH\x07r\x05\n\x03\x66ooR\x05\x63onst\x12?\n\x06nested\x18\x02 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x06nested\"_\n\x0bMessageNone\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.MessageNone.NoneMsgR\x03val\x1a\t\n\x07NoneMsg\"3\n\x0fMessageDisabled\x12\x19\n\x03val\x18\x01 \x01(\x04\x42\x07\xbaH\x04\x32\x02 {R\x03val:\x05\xbaH\x02\x08\x01\"D\n\x07Message\x12\x39\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x03val\"\\\n\x13MessageCrossPackage\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.other_package.EmbedR\x03val\"P\n\x0bMessageSkip\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xd8\x01\x03R\x03val\"T\n\x0fMessageRequired\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01R\x03val\"l\n\x1aMessageRequiredButOptional\x12\x46\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03val\x88\x01\x01\x42\x06\n\x04_val\"i\n\x14MessageRequiredOneof\x12\x43\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03valB\x0c\n\x03one\x12\x05\xbaH\x02\x08\x01\"\x15\n\x13MessageWith3dInside\"g\n\x17MessageOneofSingleField\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x10\xbaH\r\"\x0b\n\tstr_field\"q\n\x1fMessageOneofSingleFieldRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x12\xbaH\x0f\"\r\n\tstr_field\x10\x01\"v\n\x1aMessageOneofMultipleFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1c\xbaH\x19\"\x17\n\tstr_field\n\nbool_field\"\x80\x01\n\"MessageOneofMultipleFieldsRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1e\xbaH\x1b\"\x19\n\tstr_field\n\nbool_field\x10\x01\"\xb5\x01\n MessageOneofMultipleSharedFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField\x12\x1b\n\tint_field\x18\x03 \x01(\x05R\x08intField:8\xbaH5\"\x19\n\tstr_field\n\nbool_field\x10\x01\"\x18\n\tstr_field\n\tint_field\x10\x01\"G\n\x1cMessageOneofUnknownFieldName\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField:\n\xbaH\x07\"\x05\n\x03xxx\"\x81\x01\n\x1aMessageOneofDuplicateField\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\'\xbaH$\"\"\n\tstr_field\n\nbool_field\n\tstr_field\"[\n\x16MessageOneofZeroFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x05\xbaH\x02\"\x00\"h\n\x19MessageOneofUnsatisfiable\x12\x0c\n\x01\x61\x18\x01 \x01(\x08R\x01\x61\x12\x0c\n\x01\x62\x18\x02 \x01(\x08R\x01\x62\x12\x0c\n\x01\x63\x18\x03 \x01(\x08R\x01\x63:!\xbaH\x1e\"\x08\n\x01\x61\n\x01\x62\x10\x01\"\x08\n\x01\x62\n\x01\x63\x10\x01\"\x08\n\x01\x61\n\x01\x63\x10\x01\x42\xcf\x01\n\"com.buf.validate.conformance.casesB\rMessagesProtoP\x01\xa2\x02\x04\x42VCC\xaa\x02\x1e\x42uf.Validate.Conformance.Cases\xca\x02\x1e\x42uf\\Validate\\Conformance\\Cases\xe2\x02*Buf\\Validate\\Conformance\\Cases\\GPBMetadata\xea\x02!Buf::Validate::Conformance::Casesb\x06proto3') +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n-buf/validate/conformance/cases/messages.proto\x12\x1e\x62uf.validate.conformance.cases\x1a\x38\x62uf/validate/conformance/cases/other_package/embed.proto\x1a\x1b\x62uf/validate/validate.proto\"l\n\x07TestMsg\x12 \n\x05\x63onst\x18\x01 \x01(\tB\n\xbaH\x07r\x05\n\x03\x66ooR\x05\x63onst\x12?\n\x06nested\x18\x02 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x06nested\"_\n\x0bMessageNone\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.MessageNone.NoneMsgR\x03val\x1a\t\n\x07NoneMsg\"3\n\x0fMessageDisabled\x12\x19\n\x03val\x18\x01 \x01(\x04\x42\x07\xbaH\x04\x32\x02 {R\x03val:\x05\xbaH\x02\x08\x01\"D\n\x07Message\x12\x39\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgR\x03val\"\\\n\x13MessageCrossPackage\x12\x45\n\x03val\x18\x01 \x01(\x0b\x32\x33.buf.validate.conformance.cases.other_package.EmbedR\x03val\"P\n\x0bMessageSkip\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xd8\x01\x03R\x03val\"T\n\x0fMessageRequired\x12\x41\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01R\x03val\"l\n\x1aMessageRequiredButOptional\x12\x46\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03val\x88\x01\x01\x42\x06\n\x04_val\"i\n\x14MessageRequiredOneof\x12\x43\n\x03val\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xc8\x01\x01H\x00R\x03valB\x0c\n\x03one\x12\x05\xbaH\x02\x08\x01\"\x15\n\x13MessageWith3dInside\"g\n\x17MessageOneofSingleField\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x10\xbaH\r\"\x0b\n\tstr_field\"q\n\x1fMessageOneofSingleFieldRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x12\xbaH\x0f\"\r\n\tstr_field\x10\x01\"v\n\x1aMessageOneofMultipleFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1c\xbaH\x19\"\x17\n\tstr_field\n\nbool_field\"\x80\x01\n\"MessageOneofMultipleFieldsRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1e\xbaH\x1b\"\x19\n\tstr_field\n\nbool_field\x10\x01\"\xb5\x01\n MessageOneofMultipleSharedFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField\x12\x1b\n\tint_field\x18\x03 \x01(\x05R\x08intField:8\xbaH5\"\x19\n\tstr_field\n\nbool_field\x10\x01\"\x18\n\tstr_field\n\tint_field\x10\x01\"G\n\x1cMessageOneofUnknownFieldName\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField:\n\xbaH\x07\"\x05\n\x03xxx\"\x81\x01\n\x1aMessageOneofDuplicateField\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\'\xbaH$\"\"\n\tstr_field\n\nbool_field\n\tstr_field\"[\n\x16MessageOneofZeroFields\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x05\xbaH\x02\"\x00\"h\n\x19MessageOneofUnsatisfiable\x12\x0c\n\x01\x61\x18\x01 \x01(\x08R\x01\x61\x12\x0c\n\x01\x62\x18\x02 \x01(\x08R\x01\x62\x12\x0c\n\x01\x63\x18\x03 \x01(\x08R\x01\x63:!\xbaH\x1e\"\x08\n\x01\x61\n\x01\x62\x10\x01\"\x08\n\x01\x62\n\x01\x63\x10\x01\"\x08\n\x01\x61\n\x01\x63\x10\x01\"\x82\x01\n\x1dMessageOneofIgnoreUnpopulated\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12&\n\nbool_field\x18\x02 \x01(\x08\x42\x07\xbaH\x04j\x02\x08\x01R\tboolField:\x1c\xbaH\x19\"\x17\n\tstr_field\n\nbool_field\"\x8c\x01\n%MessageOneofIgnoreUnpopulatedRequired\x12\x1b\n\tstr_field\x18\x01 \x01(\tR\x08strField\x12&\n\nbool_field\x18\x02 \x01(\x08\x42\x07\xbaH\x04j\x02\x08\x01R\tboolField:\x1e\xbaH\x1b\"\x19\n\tstr_field\n\nbool_field\x10\x01\"\xa7\x01\n\x1aMessageOneofIgnoreOverride\x12L\n\tmsg_field\x18\x01 \x01(\x0b\x32\'.buf.validate.conformance.cases.TestMsgB\x06\xbaH\x03\xd8\x01\x03R\x08msgField\x12\x1d\n\nbool_field\x18\x02 \x01(\x08R\tboolField:\x1c\xbaH\x19\"\x17\n\tmsg_field\n\nbool_fieldB\xcf\x01\n\"com.buf.validate.conformance.casesB\rMessagesProtoP\x01\xa2\x02\x04\x42VCC\xaa\x02\x1e\x42uf.Validate.Conformance.Cases\xca\x02\x1e\x42uf\\Validate\\Conformance\\Cases\xe2\x02*Buf\\Validate\\Conformance\\Cases\\GPBMetadata\xea\x02!Buf::Validate::Conformance::Casesb\x06proto3') _globals = globals() _builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) @@ -82,6 +82,18 @@ _globals['_MESSAGEONEOFZEROFIELDS']._serialized_options = b'\272H\002\"\000' _globals['_MESSAGEONEOFUNSATISFIABLE']._loaded_options = None _globals['_MESSAGEONEOFUNSATISFIABLE']._serialized_options = b'\272H\036\"\010\n\001a\n\001b\020\001\"\010\n\001b\n\001c\020\001\"\010\n\001a\n\001c\020\001' + _globals['_MESSAGEONEOFIGNOREUNPOPULATED'].fields_by_name['bool_field']._loaded_options = None + _globals['_MESSAGEONEOFIGNOREUNPOPULATED'].fields_by_name['bool_field']._serialized_options = b'\272H\004j\002\010\001' + _globals['_MESSAGEONEOFIGNOREUNPOPULATED']._loaded_options = None + _globals['_MESSAGEONEOFIGNOREUNPOPULATED']._serialized_options = b'\272H\031\"\027\n\tstr_field\n\nbool_field' + _globals['_MESSAGEONEOFIGNOREUNPOPULATEDREQUIRED'].fields_by_name['bool_field']._loaded_options = None + _globals['_MESSAGEONEOFIGNOREUNPOPULATEDREQUIRED'].fields_by_name['bool_field']._serialized_options = b'\272H\004j\002\010\001' + _globals['_MESSAGEONEOFIGNOREUNPOPULATEDREQUIRED']._loaded_options = None + _globals['_MESSAGEONEOFIGNOREUNPOPULATEDREQUIRED']._serialized_options = b'\272H\033\"\031\n\tstr_field\n\nbool_field\020\001' + _globals['_MESSAGEONEOFIGNOREOVERRIDE'].fields_by_name['msg_field']._loaded_options = None + _globals['_MESSAGEONEOFIGNOREOVERRIDE'].fields_by_name['msg_field']._serialized_options = b'\272H\003\330\001\003' + _globals['_MESSAGEONEOFIGNOREOVERRIDE']._loaded_options = None + _globals['_MESSAGEONEOFIGNOREOVERRIDE']._serialized_options = b'\272H\031\"\027\n\tmsg_field\n\nbool_field' _globals['_TESTMSG']._serialized_start=168 _globals['_TESTMSG']._serialized_end=276 _globals['_MESSAGENONE']._serialized_start=278 @@ -122,4 +134,10 @@ _globals['_MESSAGEONEOFZEROFIELDS']._serialized_end=1951 _globals['_MESSAGEONEOFUNSATISFIABLE']._serialized_start=1953 _globals['_MESSAGEONEOFUNSATISFIABLE']._serialized_end=2057 + _globals['_MESSAGEONEOFIGNOREUNPOPULATED']._serialized_start=2060 + _globals['_MESSAGEONEOFIGNOREUNPOPULATED']._serialized_end=2190 + _globals['_MESSAGEONEOFIGNOREUNPOPULATEDREQUIRED']._serialized_start=2193 + _globals['_MESSAGEONEOFIGNOREUNPOPULATEDREQUIRED']._serialized_end=2333 + _globals['_MESSAGEONEOFIGNOREOVERRIDE']._serialized_start=2336 + _globals['_MESSAGEONEOFIGNOREOVERRIDE']._serialized_end=2503 # @@protoc_insertion_point(module_scope) diff --git a/gen/buf/validate/conformance/cases/messages_pb2.pyi b/gen/buf/validate/conformance/cases/messages_pb2.pyi index 618c724b..987257ff 100644 --- a/gen/buf/validate/conformance/cases/messages_pb2.pyi +++ b/gen/buf/validate/conformance/cases/messages_pb2.pyi @@ -157,3 +157,27 @@ class MessageOneofUnsatisfiable(_message.Message): b: bool c: bool def __init__(self, a: bool = ..., b: bool = ..., c: bool = ...) -> None: ... + +class MessageOneofIgnoreUnpopulated(_message.Message): + __slots__ = ("str_field", "bool_field") + STR_FIELD_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_FIELD_NUMBER: _ClassVar[int] + str_field: str + bool_field: bool + def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ...) -> None: ... + +class MessageOneofIgnoreUnpopulatedRequired(_message.Message): + __slots__ = ("str_field", "bool_field") + STR_FIELD_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_FIELD_NUMBER: _ClassVar[int] + str_field: str + bool_field: bool + def __init__(self, str_field: _Optional[str] = ..., bool_field: bool = ...) -> None: ... + +class MessageOneofIgnoreOverride(_message.Message): + __slots__ = ("msg_field", "bool_field") + MSG_FIELD_FIELD_NUMBER: _ClassVar[int] + BOOL_FIELD_FIELD_NUMBER: _ClassVar[int] + msg_field: TestMsg + bool_field: bool + def __init__(self, msg_field: _Optional[_Union[TestMsg, _Mapping]] = ..., bool_field: bool = ...) -> None: ... diff --git a/protovalidate/internal/rules.py b/protovalidate/internal/rules.py index 55d76408..7f726412 100644 --- a/protovalidate/internal/rules.py +++ b/protovalidate/internal/rules.py @@ -1047,10 +1047,13 @@ def _new_field_rule( def _new_rules(self, desc: descriptor.Descriptor) -> list[Rules]: result: list[Rules] = [] rule: typing.Optional[Rules] = None + all_msg_oneof_fields = set() if validate_pb2.message in desc.GetOptions().Extensions: message_level = desc.GetOptions().Extensions[validate_pb2.message] if message_level.disabled: return [] + for oneof in message_level.oneof: + all_msg_oneof_fields.update(oneof.fields) if rule := self._new_message_rule(message_level, desc): result.append(rule) @@ -1062,6 +1065,11 @@ def _new_rules(self, desc: descriptor.Descriptor) -> list[Rules]: for field in desc.fields: if validate_pb2.field in field.GetOptions().Extensions: field_level = field.GetOptions().Extensions[validate_pb2.field] + if not field_level.HasField("ignore") and field.name in all_msg_oneof_fields: + field_level_override = validate_pb2.FieldRules() + field_level_override.CopyFrom(field_level) + field_level_override.ignore = validate_pb2.IGNORE_IF_UNPOPULATED + field_level = field_level_override if field_level.ignore == validate_pb2.IGNORE_ALWAYS: continue result.append(self._new_field_rule(field, field_level)) From 9d5eb7be635e76157ee007d8b436f7c63e14a3e6 Mon Sep 17 00:00:00 2001 From: Steve Ayers Date: Mon, 16 Jun 2025 13:48:24 -0400 Subject: [PATCH 7/7] Remove flag parsing --- protovalidate/internal/matches.py | 39 +------------------------------ tests/matches_test.py | 10 -------- 2 files changed, 1 insertion(+), 48 deletions(-) diff --git a/protovalidate/internal/matches.py b/protovalidate/internal/matches.py index 9c785597..72728582 100644 --- a/protovalidate/internal/matches.py +++ b/protovalidate/internal/matches.py @@ -12,9 +12,7 @@ # See the License for the specific language governing permissions and # limitations under the License. -import operator import re -from functools import reduce import celpy from celpy import celtypes @@ -34,20 +32,6 @@ r"\[\\b.*\]", # Backspace eg: [\b] ] -# Regex for searching a regex pattern for flags. -flag_pattern = re.compile(r"^\(\?(?P[ims\-]+)\)") - -# See https://docs.python.org/3/library/re.html#flags -flag_mapping = { - "a": re.A, - "i": re.I, - "L": re.L, - "m": re.M, - "s": re.S, - "u": re.U, - "x": re.X, -} - def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: """Return True if the given pattern matches text. False otherwise. @@ -73,30 +57,9 @@ def cel_matches(text: celtypes.Value, pattern: celtypes.Value) -> celpy.Result: if r is not None: msg = f"error evaluating pattern {pattern}, invalid RE2 syntax" raise celpy.CELEvalError(msg) - # The conformance tests use flags at the very beginning of the sequence, which - # is likely the most common place where this rare feature will be used. - # - # So we check for the flags at the very beginning and if present, apply them - # using Python re enums. - flags = "" - flag_matches = re.match(flag_pattern, pattern) - if flag_matches is not None: - flag_group = flag_matches.groupdict()["flags"] - for fl in flag_group: - # Flag removal, don't include it in the output - if fl == "-": - continue - flags += fl - # Grab the rest of the expression minus the flags - pattern_str = pattern[len(flag_matches[0]) :] - # Convert a string of flags (i.e. aiLm) into the actual re.A, re.I enums - flags_enums = reduce(operator.or_, (flag_mapping[c] for c in flags if c in flag_mapping), 0) - exp = re.compile(pattern_str, flags=flags_enums) - else: - exp = re.compile(pattern) try: - m = re.search(exp, text) + m = re.search(pattern, text) except re.error as ex: return celpy.CELEvalError("match error", ex.__class__, ex.args) diff --git a/tests/matches_test.py b/tests/matches_test.py index 87818cca..7e3a91aa 100644 --- a/tests/matches_test.py +++ b/tests/matches_test.py @@ -42,13 +42,3 @@ def test_invalid_re2_syntax(self): self.fail(f"expected an error on pattern {cel_pattern}") except celpy.CELEvalError as e: self.assertEqual(str(e), f"error evaluating pattern {cel_pattern}, invalid RE2 syntax") - - def test_flags(self) -> None: - self.assertTrue(extra_func.cel_matches(celtypes.StringType("foobar"), celtypes.StringType("(?i:foo)(?-i:bar)"))) - self.assertTrue(extra_func.cel_matches(celtypes.StringType("FOObar"), celtypes.StringType("(?i:foo)(?-i:bar)"))) - self.assertFalse( - extra_func.cel_matches(celtypes.StringType("fooBAR"), celtypes.StringType("(?i:foo)(?-i:bar)")) - ) - self.assertFalse( - extra_func.cel_matches(celtypes.StringType("FOOBAR"), celtypes.StringType("(?i:foo)(?-i:bar)")) - )