From 4c3b12ea2b07ee8b01f60c123d76582260e486c4 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 13:42:47 -0700 Subject: [PATCH 1/8] fix wildcard handling --- src/jsonata/jsonata.py | 3 --- src/jsonata/utils.py | 6 +----- tests/array_test.py | 15 +++++++++++++++ 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/src/jsonata/jsonata.py b/src/jsonata/jsonata.py index 55476e0..66687e5 100644 --- a/src/jsonata/jsonata.py +++ b/src/jsonata/jsonata.py @@ -684,9 +684,6 @@ def evaluate_wildcard(self, expr: Optional[parser.Parser.Symbol], input: Optiona if isinstance(value, list): value = self.flatten(value, None) results = functions.Functions.append(results, value) - elif isinstance(value, dict): - # Call recursively do decompose the map - results.extend(self.evaluate_wildcard(expr, value)) else: results.append(value) diff --git a/src/jsonata/utils.py b/src/jsonata/utils.py index eaab3e2..e886b18 100644 --- a/src/jsonata/utils.py +++ b/src/jsonata/utils.py @@ -74,11 +74,7 @@ def is_function(o: Optional[Any]) -> bool: @staticmethod def create_sequence(el: Optional[Any] = NONE) -> list: if el is not Utils.NONE: - if isinstance(el, list) and len(el) == 1: - sequence = Utils.JList(el) - else: - # This case does NOT exist in Javascript! Why? - sequence = Utils.JList([el]) + sequence = Utils.JList([el]) else: sequence = Utils.JList() sequence.sequence = True diff --git a/tests/array_test.py b/tests/array_test.py index d999c0a..9f72ba4 100644 --- a/tests/array_test.py +++ b/tests/array_test.py @@ -5,3 +5,18 @@ class TestArray: def test_array(self): assert jsonata.Jsonata("$.[{ }] ~> $reduce($append)").evaluate([True, True]) == [{}, {}] + + def test_wildcard(self): + expr = jsonata.Jsonata("*") + assert expr.evaluate([{"x": 1}]) == {"x": 1} + + def test_wildcard_filter(self): + value1 = {"value": {"Name": "Cell1", "Product": "Product1"}} + value2 = {"value": {"Name": "Cell2", "Product": "Product2"}} + data = [value1, value2] + + expression = jsonata.Jsonata("*[value.Product = 'Product1']") + assert expression.evaluate(data) == value1 + + expression2 = jsonata.Jsonata("**[value.Product = 'Product1']") + assert expression2.evaluate(data) == value1 From e164d24225a51c55481bf560f1714c7425426ab7 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 13:56:42 -0700 Subject: [PATCH 2/8] avoid tuplebindings to be empty --- src/jsonata/jsonata.py | 2 +- tests/array_test.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/jsonata/jsonata.py b/src/jsonata/jsonata.py index 66687e5..df370e4 100644 --- a/src/jsonata/jsonata.py +++ b/src/jsonata/jsonata.py @@ -469,7 +469,7 @@ def evaluate_tuple_step(self, expr: parser.Parser.Symbol, input: Optional[Sequen result.tuple_stream = True step_env = environment if tuple_bindings is None: - tuple_bindings = [{"@": item} for item in input if item is not None] + tuple_bindings = [{"@": item} for item in input] for tuple_binding in tuple_bindings: step_env = self.create_frame_from_tuple(environment, tuple_binding) diff --git a/tests/array_test.py b/tests/array_test.py index 9f72ba4..55af403 100644 --- a/tests/array_test.py +++ b/tests/array_test.py @@ -10,6 +10,11 @@ def test_wildcard(self): expr = jsonata.Jsonata("*") assert expr.evaluate([{"x": 1}]) == {"x": 1} + def test_index(self): + expr = jsonata.Jsonata("($x:=['a','b']; $x#$i.$i)") + assert expr.evaluate(1) == [0, 1] + assert expr.evaluate(None) == [0, 1] + def test_wildcard_filter(self): value1 = {"value": {"Name": "Cell1", "Product": "Product1"}} value2 = {"value": {"Name": "Cell2", "Product": "Product2"}} From 803868d923f0e39450696c8c5ecc8c21c9d6c03d Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 14:07:34 -0700 Subject: [PATCH 3/8] Fix index increment in Signature validation --- src/jsonata/signature.py | 1 + tests/signature_test.py | 12 +++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/jsonata/signature.py b/src/jsonata/signature.py index 303a827..cd58c57 100644 --- a/src/jsonata/signature.py +++ b/src/jsonata/signature.py @@ -308,6 +308,7 @@ def validate(self, args: Any, context: Optional[Any]) -> Optional[Any]: arg = args[arg_index] if arg_index < len(args) else None validated_args.append(arg) arg_index += 1 + index += 1 return validated_args self.throw_validation_error(args, supplied_sig, self.function_name) diff --git a/tests/signature_test.py b/tests/signature_test.py index 5a61532..53be04c 100644 --- a/tests/signature_test.py +++ b/tests/signature_test.py @@ -7,7 +7,7 @@ class TestSignature: def test_parameters_are_converted_to_arrays(self): expr = jsonata.Jsonata("$greet(1,null,3)") expr.register_function("greet", jsonata.Jsonata.JFunction(TestSignature.JFunctionCallable1(), "")) - assert expr.evaluate(None) == "[[1], [null], [3], [None]]" + assert expr.evaluate(None) == "[[1], None, [3], None]" class JFunctionCallable1(jsonata.Jsonata.JFunctionCallable): @@ -30,3 +30,13 @@ class JFunctionCallable2(jsonata.Jsonata.JFunctionCallable): def call(self, input, args): return None + + def test_var_arg_many(self): + expr = jsonata.Jsonata("$customArgs('test',[1,2,3,4],3)") + expr.register_function("customArgs", jsonata.Jsonata.JFunction(TestSignature.JFunctionCallable3(), "n:s>")) + assert expr.evaluate(None) == "['test', [1, 2, 3, 4], 3]" + + class JFunctionCallable3(jsonata.Jsonata.JFunctionCallable): + + def call(self, input, args): + return str(args) From 505cba03eae3a6a5af74dcb76a95334a5c82c79f Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 14:48:18 -0700 Subject: [PATCH 4/8] escape special chars in field names --- tests/string_test.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/string_test.py b/tests/string_test.py index 7eb4deb..149768a 100644 --- a/tests/string_test.py +++ b/tests/string_test.py @@ -84,6 +84,11 @@ def test_split(self): res = jsonata.Jsonata("$split('this.*.*is.*a.*test.*.*.*.*.*.*', /\\.\\*/, 8)").evaluate(None) assert res == ["this", "", "is", "a", "test", "", "", ""] + def test_fieldname_with_special_char(self): + expr = jsonata.Jsonata("$ ~> |$|{}|") + o = {"a\nb": "c\nd"} + assert expr.evaluate(o) == o + def test_trim(self): assert jsonata.Jsonata("$trim(\"\n\")").evaluate(None) == "" assert jsonata.Jsonata("$trim(\" \")").evaluate(None) == "" From e94af597dbd8fec02fd820c1517b29e1d6a6a6e1 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 14:55:52 -0700 Subject: [PATCH 5/8] negative index --- .gitignore | 2 ++ src/jsonata/jsonata.py | 2 +- tests/array_test.py | 6 ++++++ 3 files changed, 9 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index d613bf7..45e325b 100644 --- a/.gitignore +++ b/.gitignore @@ -17,3 +17,5 @@ venv/ .idea .venv tests/gen + +.claude diff --git a/src/jsonata/jsonata.py b/src/jsonata/jsonata.py index df370e4..d087a7c 100644 --- a/src/jsonata/jsonata.py +++ b/src/jsonata/jsonata.py @@ -519,7 +519,7 @@ def evaluate_filter(self, predicate: Optional[Any], input: Optional[Any], enviro if index < 0: # count in from end of array index = len(input) + index - item = input[index] if index < len(input) else None + item = input[index] if 0 <= index < len(input) else None if item is not None: if isinstance(item, list): results = item diff --git a/tests/array_test.py b/tests/array_test.py index 55af403..7e775e2 100644 --- a/tests/array_test.py +++ b/tests/array_test.py @@ -3,6 +3,12 @@ class TestArray: + def test_negative_index(self): + expr = jsonata.Jsonata("item[-1]") + assert expr.evaluate({"item": []}) is None + expr = jsonata.Jsonata("$[-1]") + assert expr.evaluate([]) is None + def test_array(self): assert jsonata.Jsonata("$.[{ }] ~> $reduce($append)").evaluate([True, True]) == [{}, {}] From d2bfc4c98f28b8a33d203b14d015d5b29da9f944 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 15:08:29 -0700 Subject: [PATCH 6/8] Update assertFn to use custom error message --- src/jsonata/functions.py | 7 ++++--- src/jsonata/jexception.py | 3 +++ tests/array_test.py | 7 +++++++ 3 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/jsonata/functions.py b/src/jsonata/functions.py index c3f29ad..d44b115 100644 --- a/src/jsonata/functions.py +++ b/src/jsonata/functions.py @@ -1836,7 +1836,8 @@ def each(obj: Optional[Mapping], func: Any) -> Optional[list]: # @staticmethod def error(message: Optional[str]) -> NoReturn: - raise jexception.JException("D3137", -1, message if message is not None else "$error() function evaluated") + raise jexception.JException("D3137", -1, + message if message is not None else "$error() function evaluated") # # @@ -1851,8 +1852,8 @@ def assert_fn(condition: Optional[bool], message: Optional[str]) -> None: raise jexception.JException("T0410", -1) if not condition: - raise jexception.JException("D3141", -1, "$assert() statement failed") - # message: message || "$assert() statement failed" + raise jexception.JException("D3141", -1, + message if message is not None else "$assert() statement failed") # # diff --git a/src/jsonata/jexception.py b/src/jsonata/jexception.py index e0635d4..ab84242 100644 --- a/src/jsonata/jexception.py +++ b/src/jsonata/jexception.py @@ -105,6 +105,9 @@ def msg(error: str, location: int, arg1: Optional[Any], arg2: Optional[Any], det formatted = message + if formatted == "{{{message}}}": + return str(arg1) + # Replace any {{var}} with format "{}" formatted = re.sub("\\{\\{\\w+\\}\\}", "{}", formatted) diff --git a/tests/array_test.py b/tests/array_test.py index 7e775e2..1b01aae 100644 --- a/tests/array_test.py +++ b/tests/array_test.py @@ -1,4 +1,5 @@ import jsonata +import pytest class TestArray: @@ -31,3 +32,9 @@ def test_wildcard_filter(self): expression2 = jsonata.Jsonata("**[value.Product = 'Product1']") assert expression2.evaluate(data) == value1 + + def test_assert_custom_message(self): + expr = jsonata.Jsonata("$assert(false, 'custom error')") + with pytest.raises(jsonata.JException) as exc_info: + expr.evaluate(None) + assert "custom error" in str(exc_info.value) From b6cc981280d269d676d19bc938e54d3e57d421b9 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 15:18:13 -0700 Subject: [PATCH 7/8] add check value of position in while cycle after increment --- src/jsonata/tokenizer.py | 5 ++++- tests/string_test.py | 10 ++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/jsonata/tokenizer.py b/src/jsonata/tokenizer.py index 62027b7..0fad3db 100644 --- a/src/jsonata/tokenizer.py +++ b/src/jsonata/tokenizer.py @@ -137,7 +137,10 @@ def scan_regex(self) -> re.Pattern: if pattern == "": raise jexception.JException("S0301", self.position) self.position += 1 - current_char = self.path[self.position] + if self.position < self.length: + current_char = self.path[self.position] + else: + current_char = None # flags start = self.position while current_char == 'i' or current_char == 'm': diff --git a/tests/string_test.py b/tests/string_test.py index 149768a..878569e 100644 --- a/tests/string_test.py +++ b/tests/string_test.py @@ -42,6 +42,16 @@ def test_regex(self): assert (jsonata.Jsonata("($matcher := $eval('/^' & 'foo' & '/i'); $.$spread()[$.$keys() ~> $matcher])") .evaluate({"foo": 1, "bar": 2}) == {"foo": 1}) + def test_regex_literal(self): + expr = jsonata.Jsonata("/^test.*$/") + result = expr.evaluate(None) + assert result.pattern == "^test.*$" + + def test_eval_regex(self): + expr = jsonata.Jsonata("$eval('/^test.*$/')") + result = expr.evaluate(None) + assert result.pattern == "^test.*$" + # # Additional $split tests # From 157df79ceb70f711bb4ebcaecbb16922c0536de4 Mon Sep 17 00:00:00 2001 From: Robert Yokota Date: Sat, 14 Mar 2026 15:30:05 -0700 Subject: [PATCH 8/8] fix regex matcher body and next func --- src/jsonata/jsonata.py | 22 +++++++++++++++++++++- src/jsonata/parser.py | 3 ++- tests/string_test.py | 17 +++++++++++++++++ 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/jsonata/jsonata.py b/src/jsonata/jsonata.py index d087a7c..ed99a66 100644 --- a/src/jsonata/jsonata.py +++ b/src/jsonata/jsonata.py @@ -1472,7 +1472,14 @@ def apply_inner(self, proc: Optional[Any], args: Optional[Any], input: Optional[ elif isinstance(proc, Jsonata.JLambda): result = proc.call(input, validated_args) elif isinstance(proc, re.Pattern): - result = [s for s in validated_args if proc.search(s) is not None] + _res = [] + for s in validated_args: + if isinstance(s, str): + _res.append(Jsonata._regex_closure(proc.finditer(s))) + if len(_res) == 1: + result = _res[0] + else: + result = _res else: print("Proc not found " + str(proc)) raise jexception.JException("T1006", 0) @@ -1486,6 +1493,19 @@ def apply_inner(self, proc: Optional[Any], args: Optional[Any], input: Optional[ raise err return result + @staticmethod + def _regex_closure(iterator): + m = next(iterator, None) + if m is None: + return None + return { + "match": m.group(), + "start": m.start(), + "end": m.end(), + "groups": [m.group()], + "next": Jsonata.JLambda(lambda: Jsonata._regex_closure(iterator)) + } + # # Evaluate lambda against input data # @param {Object} expr - JSONata expression diff --git a/src/jsonata/parser.py b/src/jsonata/parser.py index d92a4c8..9385185 100644 --- a/src/jsonata/parser.py +++ b/src/jsonata/parser.py @@ -1047,7 +1047,8 @@ def process_ast(self, expr: Optional[Symbol]) -> Optional[Symbol]: rest = self.process_ast(expr.rhs) if (rest.type == "function" and rest.procedure.type == "path" and len( rest.procedure.steps) == 1 and rest.procedure.steps[0].type == "name" and - result.steps[-1].type == "function"): + result.steps[-1].type == "function" and + isinstance(rest.procedure.steps[0].value, Parser.Symbol)): # next function in chain of functions - will override a thenable result.steps[-1].next_function = rest.procedure.steps[0].value if rest.type == "path": diff --git a/tests/string_test.py b/tests/string_test.py index 878569e..76de5cb 100644 --- a/tests/string_test.py +++ b/tests/string_test.py @@ -52,6 +52,23 @@ def test_eval_regex(self): result = expr.evaluate(None) assert result.pattern == "^test.*$" + def test_eval_regex_check_answer_data(self): + expr = jsonata.Jsonata("(\n $matcher := $eval('/l/');\n ('Hello World' ~> $matcher);\n)") + result = expr.evaluate(None) + assert result["match"] == "l" + assert result["start"] == 2 + assert result["end"] == 3 + assert result["groups"] == ["l"] + assert callable(result["next"].function) + + def test_eval_regex_call_next_and_check_result(self): + expr = jsonata.Jsonata("(\n $matcher := $eval('/l/');\n ('Hello World' ~> $matcher).next();\n)") + result = expr.evaluate(None) + assert result["match"] == "l" + assert result["start"] == 3 + assert result["end"] == 4 + assert result["groups"] == ["l"] + # # Additional $split tests #