diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1b44c26..c783176 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -17,7 +17,7 @@ jobs: fail-fast: false matrix: python_version: - - "3.10" + - "3.11" steps: - name: Checkout uses: actions/checkout@v2 diff --git a/README.md b/README.md index d0aaa0e..076e1e4 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,7 @@ This is a fork of the original [`airspeed`](https://github.com/purcell/airspeed) ⚠️ Note: This fork of `airspeed` focuses on providing maximum parity with AWS' implementation of Velocity templates (used in, e.g., API Gateway or AppSync). In some cases, the behavior may diverge from the VTL spec, or from the Velocity [reference implementation](https://velocity.apache.org/download.cgi). ## Change Log: +* v0.6.6: add support for `$string.matches( $pattern )`; fix bug where some escaped character would prevent string matching * v0.6.5: handle `$map.put('key', null)` correctly * v0.6.4: add support for string.indexOf, string.substring and array.isEmpty * v0.6.3: array notation for dicts using string literals and merge upstream patches diff --git a/airspeed/operators.py b/airspeed/operators.py index 7a5d9ba..7b614c5 100644 --- a/airspeed/operators.py +++ b/airspeed/operators.py @@ -40,6 +40,7 @@ def dict_to_string(obj: dict) -> str: "contains": lambda self, value: value in self, "indexOf": lambda self, ch: self.index(ch), "substring": lambda self, start, end=None: self[start:end], + "matches": lambda self, pattern: bool(re.fullmatch(pattern, self)), }, list: { "size": lambda self: len(self), @@ -455,7 +456,7 @@ def calculate(self, namespace, loader): class StringLiteral(_Element): - STRING = re.compile(r"'((?:\\['nrbt\\\\\\$]|[^'\\])*)'(.*)", re.S) + STRING = re.compile(r"'([^']*)'(.*)", re.S) ESCAPED_CHAR = re.compile(r"\\([nrbt'\\])") def parse(self): diff --git a/setup.py b/setup.py index 9acb1b8..2af65c8 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="airspeed-ext", - version="0.6.5", + version="0.6.6", description=( "Airspeed is a powerful and easy-to-use templating engine " "for Python that aims for a high level of compatibility " @@ -30,6 +30,7 @@ "flake8-isort", "pytest", "pytest-httpserver", + "localstack-snapshot", ]}, test_suite="tests", tests_require=[], diff --git a/tests/conftest.py b/tests/conftest.py index 4ccd8ae..57fc2ae 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,12 +2,12 @@ from localstack.testing.aws.util import ( base_aws_client_factory, base_aws_session, - primary_testing_aws_client, + base_testing_aws_client, ) pytest_plugins = [ "localstack.testing.pytest.fixtures", - "localstack.testing.pytest.snapshot", + "localstack_snapshot.pytest.snapshot", ] @@ -23,4 +23,9 @@ def aws_client_factory(aws_session): @pytest.fixture(scope="session") def aws_client(aws_client_factory): - return primary_testing_aws_client(aws_client_factory) + return base_testing_aws_client(aws_client_factory) + + +@pytest.fixture(scope="function") +def snapshot(_snapshot_session): + return _snapshot_session diff --git a/tests/test_templating.py b/tests/test_templating.py index 1a66539..452d0d1 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -6,8 +6,8 @@ import pytest import requests import six -from localstack.constants import TEST_AWS_ACCOUNT_ID from localstack.testing.aws.util import is_aws_cloud +from localstack.testing.config import TEST_AWS_ACCOUNT_ID from localstack.testing.pytest import fixtures from localstack.utils.archives import unzip from localstack.utils.files import save_file @@ -845,6 +845,40 @@ def test_dict_to_string(self, test_render): template = "#set( $myObject = {'k1': 'v1', 'k2': 'v2'} )$myObject.toString()" test_render(template) + def test_string_matches_true(self, test_render): + template = "#set( $myString = '123456789' )$myString.matches( '[0-9]*' )" + test_render(template) + + def test_string_matches_false(self, test_render): + template = "#set( $myString = 'd123' )$myString.matches( '[0-9]*' )" + test_render(template) + + def test_string_matches_full_date(self, test_render): + template = ( + "#set( $myString = '2020-01-20T08:00:00.000Z' )" + "$myString.matches('^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\.000Z$')" # noqa + ) + test_render(template) + + template = ( + '#set( $myString = "2020-01-20T08:00:00.000Z" )' + '$myString.matches("^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:00\.000Z$")' # noqa + ) + test_render(template) + + def test_string_with_escaped_char(self, test_render): + template = "#set( $myString = '\{\n\t\r\%\5' )$myString" # noqa + test_render(template) + + template = '#set( $myString = "\{\n\t\r\%\5" )$myString' # noqa + test_render(template) + + template = "'\{\n\t\r\%\5'" # noqa + test_render(template) + + template = '"\{\n\t\r\%\5"' # noqa + test_render(template) + class TestInternals: """ diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index 40621de..58db179 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1068,5 +1068,41 @@ "render-result-1-cli": "\n \"has-value\"\n ", "render-result-1": "\"does-not-have-value\"" } + }, + "tests/test_templating.py::TestTemplating::test_string_matches_true": { + "recorded-date": "24-10-2024, 19:59:43", + "recorded-content": { + "render-result-1-cli": "true", + "render-result-1": "true" + } + }, + "tests/test_templating.py::TestTemplating::test_string_matches_false": { + "recorded-date": "24-10-2024, 19:59:21", + "recorded-content": { + "render-result-1-cli": "false", + "render-result-1": "false" + } + }, + "tests/test_templating.py::TestTemplating::test_string_with_escaped_char": { + "recorded-date": "24-10-2024, 22:14:20", + "recorded-content": { + "render-result-1-cli": "\\{\n\t\r\\%\u0005", + "render-result-1": "\\{\n\t\r\\%\u0005", + "render-result-2-cli": "\\{\n\t\r\\%\u0005", + "render-result-2": "\\{\n\t\r\\%\u0005", + "render-result-3-cli": "'\\{\n\t\r\\%\u0005'", + "render-result-3": "'\\{\n\t\r\\%\u0005'", + "render-result-4-cli": "\"\\{\n\t\r\\%\u0005\"", + "render-result-4": "\"\\{\n\t\r\\%\u0005\"" + } + }, + "tests/test_templating.py::TestTemplating::test_string_matches_full_date": { + "recorded-date": "24-10-2024, 21:58:12", + "recorded-content": { + "render-result-1-cli": "true", + "render-result-1": "true", + "render-result-2-cli": "true", + "render-result-2": "true" + } } }