From 06d86f1e7b1a57dba4fd52013bfdccb51a9ff54c Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Fri, 16 May 2025 20:33:11 +0200 Subject: [PATCH 01/10] add tests for bracketed assignment --- tests/test_templating.py | 20 ++++++++++++++++++++ tests/test_templating.snapshot.json | 16 +++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/tests/test_templating.py b/tests/test_templating.py index 54756d6..26bd2cc 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -808,6 +808,26 @@ def test_dict_put_item(self, test_render): ) test_render(template, {"test_dict": {"k": "initial value"}}) + def test_dict_assign_item_via_brackets(self, test_render): + # The specified element is set with the given value. + # Velocity tries first the 'set' method on the element, then 'put' to make the assignment. + template = ( + "#set($test_dict = {} )" + "#set($key = 'bar')" + "#set( $test_dict[$key] = 'foo' )" + "$test_dict" + ) + test_render(template, {}) + + def test_array_assign_item_via_brackets(self, test_render): + template = ( + "#set($test_array = ['one', 'two', 'three'] )" + "#set($i = 1)" + "#set( $test_array[$i] = 'foo' )" + "$test_array" + ) + test_render(template, {}) + def test_put_null_in_map(self, test_render): template = r""" #set( $myMap = {} ) diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index e7ba4a6..af95625 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1097,7 +1097,7 @@ } }, "tests/test_templating.py::TestTemplating::test_string_matches_full_date": { - "recorded-date": "24-10-2024, 21:58:12", + "recorded-date": "16-05-2025, 18:29:38", "recorded-content": { "render-result-1-cli": "true", "render-result-1": "true", @@ -1111,5 +1111,19 @@ "render-result-1-cli": "0.05", "render-result-1": "0.05" } + }, + "tests/test_templating.py::TestTemplating::test_dict_assign_item_via_brackets": { + "recorded-date": "16-05-2025, 18:31:03", + "recorded-content": { + "render-result-1-cli": "{bar=foo}", + "render-result-1": "{bar=foo}" + } + }, + "tests/test_templating.py::TestTemplating::test_array_assign_item_via_brackets": { + "recorded-date": "16-05-2025, 18:32:56", + "recorded-content": { + "render-result-1-cli": "[one, foo, three]", + "render-result-1": "[one, foo, three]" + } } } From c0f7f7c2bb3bc028461b1f1eb2f9436e1d979c54 Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Fri, 16 May 2025 21:14:54 +0200 Subject: [PATCH 02/10] add tests for brackets, add Array.set --- airspeed/operators.py | 16 +++++++++++++++- tests/test_templating.py | 19 ++++++++++++++++++- tests/test_templating.snapshot.json | 13 ++++++++++--- 3 files changed, 43 insertions(+), 5 deletions(-) diff --git a/airspeed/operators.py b/airspeed/operators.py index 2625762..425cc92 100644 --- a/airspeed/operators.py +++ b/airspeed/operators.py @@ -32,6 +32,17 @@ def dict_put(self, key, value): return existing +def array_set(self, index, value): + try: + existing = self[index] + except IndexError: + raise TemplateExecutionError + + self[index] = value + return existing + + + def dict_to_string(obj: dict) -> str: return "{" + ", ".join([f"{k}={v}" for k, v in obj.items()]) + "}" @@ -52,6 +63,7 @@ def dict_to_string(obj: dict) -> str: "contains": lambda self, value: value in self, "add": lambda self, value: self.append(value), "isEmpty": lambda self: len(self) == 0, + "set": array_set, }, dict: { "put": dict_put, @@ -1069,18 +1081,20 @@ def evaluate_raw(self, stream, namespace, loader): # yet class Assignment(_Element): START = re.compile( - r"\s*\(\s*\$(\w*(?:\.[\w-]+|\[\"\$\w+\"\]*)*)\s*=\s*(.*)$", re.S + re.I + r"\s*\(\s*\$(\w*(?:\.[\w-]+|\[\"?\$\w+\"?\]*)*)\s*=\s*(.*)$", re.S + re.I ) END = re.compile(r"\s*\)(?:[ \t]*\r?\n)?(.*)$", re.S + re.M) def parse(self): (var_name,) = self.identity_match(self.START) + print(f"{var_name=}") self.terms = var_name.split(".") self.value = self.require_next_element(Expression, "expression") self.require_match(self.END, ")") def evaluate_raw(self, stream, namespace, loader): val = self.value.calculate(namespace, loader) + print(f"{val=} / {self.terms=}") if len(self.terms) == 1: namespace.set_inherited(self.terms[0], val) else: diff --git a/tests/test_templating.py b/tests/test_templating.py index 26bd2cc..a4d611d 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -824,7 +824,15 @@ def test_array_assign_item_via_brackets(self, test_render): "#set($test_array = ['one', 'two', 'three'] )" "#set($i = 1)" "#set( $test_array[$i] = 'foo' )" - "$test_array" + "#foreach ($item in $test_array)$item#end" + ) + test_render(template, {}) + + def test_array_set_item(self, test_render): + template = ( + "#set($test_array = ['one', 'two', 'three'] )" + "$test_array.set(1, 'foo')" + "#foreach ($item in $test_array)$item#end" ) test_render(template, {}) @@ -1514,6 +1522,15 @@ def test_get_position_strings_in_syntax_error_when_newline_before_error( else: pytest.fail("expected error") + def test_array_set_item_outside_range(self, test_render): + template = airspeed.Template( + "#set($test_array = ['one', 'two', 'three'] )" + "$test_array.set(5, 'foo')" + "$test_array" + ) + with pytest.raises(airspeed.TemplateExecutionError): + template.merge({}) + @pytest.mark.skip(reason="Invalid syntax, failing against VTL CLI and/or AWS") class TestInvalidCases: diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index af95625..0994492 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1120,10 +1120,17 @@ } }, "tests/test_templating.py::TestTemplating::test_array_assign_item_via_brackets": { - "recorded-date": "16-05-2025, 18:32:56", + "recorded-date": "16-05-2025, 19:14:01", "recorded-content": { - "render-result-1-cli": "[one, foo, three]", - "render-result-1": "[one, foo, three]" + "render-result-1-cli": "onefoothree", + "render-result-1": "onefoothree" + } + }, + "tests/test_templating.py::TestTemplating::test_array_set_item": { + "recorded-date": "16-05-2025, 19:13:41", + "recorded-content": { + "render-result-1-cli": "twoonefoothree", + "render-result-1": "twoonefoothree" } } } From 3dfef450d38e5cc933b7ed8561b1208c0fc2752e Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Tue, 27 May 2025 18:39:56 +0200 Subject: [PATCH 03/10] add test --- tests/test_templating.py | 11 +++++++++++ tests/test_templating.snapshot.json | 9 ++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/test_templating.py b/tests/test_templating.py index a4d611d..f4a8fe5 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -819,6 +819,17 @@ def test_dict_assign_item_via_brackets(self, test_render): ) test_render(template, {}) + def test_dict_assign_quoted_item_via_brackets(self, test_render): + # The specified element is set with the given value. + # Velocity tries first the 'set' method on the element, then 'put' to make the assignment. + template = ( + "#set($test_dict = {} )" + "#set($key = 'bar')" + "#set( $test_dict[\"$key\"] = 'foo' )" + "$test_dict" + ) + test_render(template, {}) + def test_array_assign_item_via_brackets(self, test_render): template = ( "#set($test_array = ['one', 'two', 'three'] )" diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index 0994492..33d3306 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1113,7 +1113,7 @@ } }, "tests/test_templating.py::TestTemplating::test_dict_assign_item_via_brackets": { - "recorded-date": "16-05-2025, 18:31:03", + "recorded-date": "27-05-2025, 16:38:00", "recorded-content": { "render-result-1-cli": "{bar=foo}", "render-result-1": "{bar=foo}" @@ -1132,5 +1132,12 @@ "render-result-1-cli": "twoonefoothree", "render-result-1": "twoonefoothree" } + }, + "tests/test_templating.py::TestTemplating::test_dict_assign_quoted_item_via_brackets": { + "recorded-date": "27-05-2025, 16:39:00", + "recorded-content": { + "render-result-1-cli": "{bar=foo}", + "render-result-1": "{bar=foo}" + } } } From 3bd712a9c45f9bf540db24b8600a05dec5f7498e Mon Sep 17 00:00:00 2001 From: Benjamin Simon Date: Tue, 27 May 2025 18:44:44 +0200 Subject: [PATCH 04/10] remove changes --- airspeed/operators.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/airspeed/operators.py b/airspeed/operators.py index 425cc92..aef0535 100644 --- a/airspeed/operators.py +++ b/airspeed/operators.py @@ -1081,20 +1081,18 @@ def evaluate_raw(self, stream, namespace, loader): # yet class Assignment(_Element): START = re.compile( - r"\s*\(\s*\$(\w*(?:\.[\w-]+|\[\"?\$\w+\"?\]*)*)\s*=\s*(.*)$", re.S + re.I + r"\s*\(\s*\$(\w*(?:\.[\w-]+|\[\"\$\w+\"\]*)*)\s*=\s*(.*)$", re.S + re.I ) END = re.compile(r"\s*\)(?:[ \t]*\r?\n)?(.*)$", re.S + re.M) def parse(self): (var_name,) = self.identity_match(self.START) - print(f"{var_name=}") self.terms = var_name.split(".") self.value = self.require_next_element(Expression, "expression") self.require_match(self.END, ")") def evaluate_raw(self, stream, namespace, loader): val = self.value.calculate(namespace, loader) - print(f"{val=} / {self.terms=}") if len(self.terms) == 1: namespace.set_inherited(self.terms[0], val) else: From d3e72824bea3a1d500ba390c563f1c12958192f6 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier Date: Wed, 28 May 2025 18:43:34 +0200 Subject: [PATCH 05/10] improves Assignment to handle brackets and variable expression --- airspeed/operators.py | 47 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 42 insertions(+), 5 deletions(-) diff --git a/airspeed/operators.py b/airspeed/operators.py index aef0535..7404a7a 100644 --- a/airspeed/operators.py +++ b/airspeed/operators.py @@ -42,7 +42,6 @@ def array_set(self, index, value): return existing - def dict_to_string(obj: dict) -> str: return "{" + ", ".join([f"{k}={v}" for k, v in obj.items()]) + "}" @@ -1080,14 +1079,47 @@ def evaluate_raw(self, stream, namespace, loader): # set($one.two().three = something) # yet class Assignment(_Element): + terms: list START = re.compile( - r"\s*\(\s*\$(\w*(?:\.[\w-]+|\[\"\$\w+\"\]*)*)\s*=\s*(.*)$", re.S + re.I + # first group stops at the first '$' encountered. self.end will be set at the first char of the variable + r"\s*(\(\s*\$)(\w*(?:\.[\w-]+|\[\"\$?\w+\"]|\[\$\w+])*\s*=\s*.*)$", re.S + re.I + ) + TERMS = re.compile( + # Breaks all terms into captured group, the last captured group is the first char after '= '. This will allow + # us to set self.end to the beginning of the next expression after finding all terms + # `($foo.bar["super"][$foo] = 'bar'` will become `[ 'foo', '.bar', '["super"]', '[$foo]', "'bar'" ]` + r"\s*\(\s*\$(\w*)(\.[\w-]+|\[\"?\$?\w+\"?])*\s*=\s*(.*)$", re.S + re.I ) END = re.compile(r"\s*\)(?:[ \t]*\r?\n)?(.*)$", re.S + re.M) def parse(self): - (var_name,) = self.identity_match(self.START) - self.terms = var_name.split(".") + self.identity_match(self.START) + matched_terms = self.TERMS.match(self._full_text, self.start) + self.terms = [] + for term in matched_terms.groups()[:-1]: + if term is None: + # Second group is optional and can be None + break + if term.startswith("."): + # handles .bar + self.end += len(term) + self.terms.append(term[1:]) + elif "$" in term: + # handles ["$foo"] and [$foo] + # skipping over '[' + self.end += 1 + # We might be handling too much with Expression and allow more than is allowed on aws, we will have + # to see if we get issues in the future. + self.terms.append(self.require_next_element(Expression, "expression")) + # skipping over ']' + self.end += 1 + else: + # handles ["super"] + self.end += len(term) + self.terms.append(term.strip('[]"')) + + # move the end to the start of the last group + self.end = matched_terms.start(self.TERMS.groups) self.value = self.require_next_element(Expression, "expression") self.require_match(self.END, ")") @@ -1098,8 +1130,13 @@ def evaluate_raw(self, stream, namespace, loader): else: cur = namespace for term in self.terms[:-1]: + if isinstance(term, Expression): + term = term.calculate(namespace, loader) cur = cur[term] - cur[self.terms[-1]] = val + last_term = self.terms[-1] + if isinstance(last_term, Expression): + last_term = last_term.calculate(namespace, loader) + cur[last_term] = val class EvaluateDirective(_Element): From 281a18b6d52f88ae036efdd8f359a86b9e710610 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier Date: Wed, 28 May 2025 18:44:17 +0200 Subject: [PATCH 06/10] update test --- tests/test_templating.py | 4 ++-- tests/test_templating.snapshot.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/test_templating.py b/tests/test_templating.py index f4a8fe5..8d22128 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -815,7 +815,7 @@ def test_dict_assign_item_via_brackets(self, test_render): "#set($test_dict = {} )" "#set($key = 'bar')" "#set( $test_dict[$key] = 'foo' )" - "$test_dict" + "$test_dict.toString()" ) test_render(template, {}) @@ -826,7 +826,7 @@ def test_dict_assign_quoted_item_via_brackets(self, test_render): "#set($test_dict = {} )" "#set($key = 'bar')" "#set( $test_dict[\"$key\"] = 'foo' )" - "$test_dict" + "$test_dict.toString()" ) test_render(template, {}) diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index 33d3306..059cfa5 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1113,7 +1113,7 @@ } }, "tests/test_templating.py::TestTemplating::test_dict_assign_item_via_brackets": { - "recorded-date": "27-05-2025, 16:38:00", + "recorded-date": "28-05-2025, 11:06:27", "recorded-content": { "render-result-1-cli": "{bar=foo}", "render-result-1": "{bar=foo}" @@ -1134,7 +1134,7 @@ } }, "tests/test_templating.py::TestTemplating::test_dict_assign_quoted_item_via_brackets": { - "recorded-date": "27-05-2025, 16:39:00", + "recorded-date": "28-05-2025, 11:06:39", "recorded-content": { "render-result-1-cli": "{bar=foo}", "render-result-1": "{bar=foo}" From bfb0659389609820e41f3a100638ace1825202a5 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier Date: Wed, 28 May 2025 20:37:17 +0200 Subject: [PATCH 07/10] fix support for nested access --- airspeed/operators.py | 24 +++++++++++------------- tests/test_templating.py | 10 ++++++++++ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/airspeed/operators.py b/airspeed/operators.py index 7404a7a..3d51629 100644 --- a/airspeed/operators.py +++ b/airspeed/operators.py @@ -1082,23 +1082,22 @@ class Assignment(_Element): terms: list START = re.compile( # first group stops at the first '$' encountered. self.end will be set at the first char of the variable - r"\s*(\(\s*\$)(\w*(?:\.[\w-]+|\[\"\$?\w+\"]|\[\$\w+])*\s*=\s*.*)$", re.S + re.I - ) - TERMS = re.compile( - # Breaks all terms into captured group, the last captured group is the first char after '= '. This will allow - # us to set self.end to the beginning of the next expression after finding all terms - # `($foo.bar["super"][$foo] = 'bar'` will become `[ 'foo', '.bar', '["super"]', '[$foo]', "'bar'" ]` - r"\s*\(\s*\$(\w*)(\.[\w-]+|\[\"?\$?\w+\"?])*\s*=\s*(.*)$", re.S + re.I + r"\s*(\(\s*\$)(\w*(?:\.[\w-]+|\[\"\$?\w+\"]|\[\$\w+])*\s*=\s*.*)$", + re.S + re.I, ) END = re.compile(r"\s*\)(?:[ \t]*\r?\n)?(.*)$", re.S + re.M) + # Allows us to match all terms. We are also matching on `=` so we can exit + TERMS = re.compile(r"(\.?\w+|\[[\"$]*\w+\"?]|=)", re.S + re.I) + TERMS_END = re.compile(r"\s*=\s*(.*)$", re.S) def parse(self): self.identity_match(self.START) - matched_terms = self.TERMS.match(self._full_text, self.start) + self.terms = [] - for term in matched_terms.groups()[:-1]: - if term is None: - # Second group is optional and can be None + for term_match in self.TERMS.finditer(self._full_text, self.start): + term = term_match.group(0) + if term == "=": + # If we matched the `=` we have gone through the whole variable definition break if term.startswith("."): # handles .bar @@ -1118,8 +1117,7 @@ def parse(self): self.end += len(term) self.terms.append(term.strip('[]"')) - # move the end to the start of the last group - self.end = matched_terms.start(self.TERMS.groups) + self.require_match(self.TERMS_END, "=") self.value = self.require_next_element(Expression, "expression") self.require_match(self.END, ")") diff --git a/tests/test_templating.py b/tests/test_templating.py index 8d22128..d93cf96 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -830,6 +830,16 @@ def test_dict_assign_quoted_item_via_brackets(self, test_render): ) test_render(template, {}) + def test_dict_assign_nested_items_via_brackets(self, test_render): + template = ( + "#set($test_dict = {} )" + "#set($key = 'bar')" + '#set( $test_dict["$key"] = {} )' + '#set($test_dict["$key"].nested = "foo")' + '$test_dict.bar["nested"]' + ) + test_render(template, {}) + def test_array_assign_item_via_brackets(self, test_render): template = ( "#set($test_array = ['one', 'two', 'three'] )" From 3e8e10aace72bd5c722664d24af4adec15dd6da8 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier Date: Thu, 29 May 2025 13:56:28 +0200 Subject: [PATCH 08/10] added snapshot --- tests/test_templating.snapshot.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index 059cfa5..7e12eac 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1139,5 +1139,12 @@ "render-result-1-cli": "{bar=foo}", "render-result-1": "{bar=foo}" } + }, + "tests/test_templating.py::TestTemplating::test_dict_assign_nested_items_via_brackets": { + "recorded-date": "29-05-2025, 11:56:01", + "recorded-content": { + "render-result-1-cli": "foo", + "render-result-1": "foo" + } } } From ff25d09d51f5a487928cd8e6c774efb2e8c7e026 Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier Date: Thu, 29 May 2025 06:51:40 -0600 Subject: [PATCH 09/10] added test and code cleanup --- airspeed/operators.py | 31 ++++++++++++++++------------- tests/test_templating.py | 8 ++++++++ tests/test_templating.snapshot.json | 7 +++++++ 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/airspeed/operators.py b/airspeed/operators.py index 3d51629..dcee2c5 100644 --- a/airspeed/operators.py +++ b/airspeed/operators.py @@ -1082,11 +1082,12 @@ class Assignment(_Element): terms: list START = re.compile( # first group stops at the first '$' encountered. self.end will be set at the first char of the variable + # Currently supported in assignment are: `$root`, `.dot`. `["bracket"]`, `[$var]` and `["$quoted_var"]` r"\s*(\(\s*\$)(\w*(?:\.[\w-]+|\[\"\$?\w+\"]|\[\$\w+])*\s*=\s*.*)$", re.S + re.I, ) END = re.compile(r"\s*\)(?:[ \t]*\r?\n)?(.*)$", re.S + re.M) - # Allows us to match all terms. We are also matching on `=` so we can exit + # Allows us to match all supported terms. We are also matching on `=` so we can exit TERMS = re.compile(r"(\.?\w+|\[[\"$]*\w+\"?]|=)", re.S + re.I) TERMS_END = re.compile(r"\s*=\s*(.*)$", re.S) @@ -1100,20 +1101,21 @@ def parse(self): # If we matched the `=` we have gone through the whole variable definition break if term.startswith("."): - # handles .bar + # handles .dot self.end += len(term) self.terms.append(term[1:]) elif "$" in term: - # handles ["$foo"] and [$foo] + # handles ["$quoted_var"] and [$var] # skipping over '[' self.end += 1 - # We might be handling too much with Expression and allow more than is allowed on aws, we will have - # to see if we get issues in the future. - self.terms.append(self.require_next_element(Expression, "expression")) + # `Value` handles a lot more than we need, but since we are pretty restrictive on the + # `identity_match`, it shouldn't be an issue. If it comes up as a problem in the future we can + # restrict the list further + self.terms.append(self.require_next_element(Value, "value")) # skipping over ']' self.end += 1 else: - # handles ["super"] + # handles ["bracket"] and root self.end += len(term) self.terms.append(term.strip('[]"')) @@ -1128,13 +1130,14 @@ def evaluate_raw(self, stream, namespace, loader): else: cur = namespace for term in self.terms[:-1]: - if isinstance(term, Expression): - term = term.calculate(namespace, loader) - cur = cur[term] - last_term = self.terms[-1] - if isinstance(last_term, Expression): - last_term = last_term.calculate(namespace, loader) - cur[last_term] = val + cur = cur[self._calculate_term(term, namespace, loader)] + cur[self._calculate_term(self.terms[-1], namespace, loader)] = val + + @staticmethod + def _calculate_term(term, namespace, loader): + if isinstance(term, Value): + return term.calculate(namespace, loader) + return term class EvaluateDirective(_Element): diff --git a/tests/test_templating.py b/tests/test_templating.py index d93cf96..11cecb1 100644 --- a/tests/test_templating.py +++ b/tests/test_templating.py @@ -840,6 +840,14 @@ def test_dict_assign_nested_items_via_brackets(self, test_render): ) test_render(template, {}) + def test_dict_assign_var_not_found(self, test_render): + template = ( + "#set($test_dict = {})" + '#set($test_dict["$key"] = "foo")' + "$test_dict.toString()" + ) + test_render(template, {}) + def test_array_assign_item_via_brackets(self, test_render): template = ( "#set($test_array = ['one', 'two', 'three'] )" diff --git a/tests/test_templating.snapshot.json b/tests/test_templating.snapshot.json index 7e12eac..a632195 100644 --- a/tests/test_templating.snapshot.json +++ b/tests/test_templating.snapshot.json @@ -1146,5 +1146,12 @@ "render-result-1-cli": "foo", "render-result-1": "foo" } + }, + "tests/test_templating.py::TestTemplating::test_dict_assign_var_not_found": { + "recorded-date": "29-05-2025, 12:07:42", + "recorded-content": { + "render-result-1-cli": "{$key=foo}", + "render-result-1": "{=foo}" + } } } From 0cb5c2ee77ea3c3b44d2b3b564bbe4759cf9a8bf Mon Sep 17 00:00:00 2001 From: Mathieu Cloutier Date: Thu, 29 May 2025 06:59:39 -0600 Subject: [PATCH 10/10] version bump --- README.md | 1 + setup.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index e4332f1..76106b1 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.8: Added support for bracket in Assignment with `#set`; Added support for `Array.set` * v0.6.7: fix support for floating point starting with a decimal; Implement `REPLACE_FORMAL_TEXT` to allow bypassing silent behavior of `FormalReference` element. * 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 diff --git a/setup.py b/setup.py index 366b9e3..78273df 100755 --- a/setup.py +++ b/setup.py @@ -5,7 +5,7 @@ setup( name="airspeed-ext", - version="0.6.7", + version="0.6.8", description=( "Airspeed is a powerful and easy-to-use templating engine " "for Python that aims for a high level of compatibility "