Skip to content

Commit 8e40f7d

Browse files
author
Steve Ayers
committed
Add re2 and tests for syntax
1 parent ab4ec31 commit 8e40f7d

File tree

5 files changed

+201
-0
lines changed

5 files changed

+201
-0
lines changed

Makefile

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,12 @@ format: install $(BIN)/license-header ## Format code
5252
.PHONY: test
5353
test: generate install gettestdata ## Run unit tests
5454
uv run -- python -m unittest
55+
$(MAKE) testextra
56+
57+
.PHONY: testextra
58+
testextra:
59+
uv pip install .[re2]
60+
uv run -- python -m unittest
5561

5662
.PHONY: conformance
5763
conformance: $(BIN)/protovalidate-conformance generate install ## Run conformance tests

protovalidate/internal/extra_func.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,12 @@
2323
from protovalidate.internal import string_format
2424
from protovalidate.internal.rules import MessageType, field_to_cel
2525

26+
_USE_RE2 = True
27+
try:
28+
import re2 # type: ignore
29+
except ImportError:
30+
_USE_RE2 = False
31+
2632
# See https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
2733
_email_regex = re.compile(
2834
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])?)*$"
@@ -1553,11 +1559,34 @@ def __peek(self, char: str) -> bool:
15531559
return self._index < len(self._string) and self._string[self._index] == char
15541560

15551561

1562+
def cel_matches_re(text: str, pattern: str) -> celpy.Result:
1563+
try:
1564+
m = re.search(pattern, text)
1565+
except re.error as ex:
1566+
return celpy.CELEvalError("match error", ex.__class__, ex.args)
1567+
1568+
return celtypes.BoolType(m is not None)
1569+
1570+
1571+
def cel_matches_re2(text: str, pattern: str) -> celpy.Result:
1572+
try:
1573+
m = re2.search(pattern, text)
1574+
except re2.error as ex:
1575+
return celpy.CELEvalError("match error", ex.__class__, ex.args)
1576+
1577+
return celtypes.BoolType(m is not None)
1578+
1579+
1580+
cel_matches = cel_matches_re2 if _USE_RE2 else cel_matches_re
1581+
1582+
15561583
def make_extra_funcs() -> dict[str, celpy.CELFunction]:
15571584
string_fmt = string_format.StringFormat()
15581585
return {
15591586
# Missing standard functions
15601587
"format": string_fmt.format,
1588+
# Overridden standard functions
1589+
"matches": cel_matches,
15611590
# protovalidate specific functions
15621591
"getField": cel_get_field,
15631592
"isNan": cel_is_nan,

pyproject.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ dependencies = [
2424
"protobuf==6.*",
2525
"cel-python==0.2.*",
2626
]
27+
[project.optional-dependencies]
28+
re2 = [
29+
"google-re2",
30+
]
2731

2832
[project.urls]
2933
Homepage = "https://github.com/bufbuild/protovalidate-python"

test/test_matches.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
# Copyright 2023-2025 Buf Technologies, Inc.
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import unittest
16+
17+
import celpy
18+
from celpy import celtypes
19+
20+
from protovalidate.internal.extra_func import cel_matches_re2, cel_matches_re
21+
22+
_USE_RE2 = True
23+
try:
24+
import re2 # type: ignore
25+
except ImportError:
26+
_USE_RE2 = False
27+
28+
class TestCollectViolations(unittest.TestCase):
29+
30+
@unittest.skipUnless(_USE_RE2, "Requires 're2'")
31+
def test_function_matches_re2(self):
32+
empty_string = celtypes.StringType("")
33+
# \z is valid re2 syntax for end of text
34+
assert cel_matches_re2(empty_string, "^\\z")
35+
# \Z is invalid re2 syntax
36+
assert isinstance(cel_matches_re2(empty_string, "^\\Z"), celpy.CELEvalError)
37+
38+
@unittest.skipUnless(_USE_RE2 is False, "Requires 're'")
39+
def test_function_matches_re(self):
40+
empty_string = celtypes.StringType("")
41+
# \z is invalid re syntax
42+
assert isinstance(cel_matches_re(empty_string, "^\\z"), celpy.CELEvalError)
43+
# \Z is valid re syntax for end of text
44+
assert cel_matches_re(empty_string, "^\\Z")

0 commit comments

Comments
 (0)