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/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/src/jsonata/jsonata.py b/src/jsonata/jsonata.py index 55476e0..ed99a66 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) @@ -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 @@ -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) @@ -1475,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) @@ -1489,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/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/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/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..1b01aae 100644 --- a/tests/array_test.py +++ b/tests/array_test.py @@ -1,7 +1,40 @@ import jsonata +import pytest 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]) == [{}, {}] + + 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"}} + 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 + + 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) 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) diff --git a/tests/string_test.py b/tests/string_test.py index 7eb4deb..76de5cb 100644 --- a/tests/string_test.py +++ b/tests/string_test.py @@ -42,6 +42,33 @@ 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.*$" + + 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 # @@ -84,6 +111,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) == ""