Skip to content

Commit b73853a

Browse files
authored
Merge pull request #31 from rayokota/fixes-2026-03-14
Incorporate fixes from jsonata-java
2 parents 7a99631 + 157df79 commit b73853a

11 files changed

Lines changed: 116 additions & 17 deletions

File tree

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,5 @@ venv/
1717
.idea
1818
.venv
1919
tests/gen
20+
21+
.claude

src/jsonata/functions.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1836,7 +1836,8 @@ def each(obj: Optional[Mapping], func: Any) -> Optional[list]:
18361836
#
18371837
@staticmethod
18381838
def error(message: Optional[str]) -> NoReturn:
1839-
raise jexception.JException("D3137", -1, message if message is not None else "$error() function evaluated")
1839+
raise jexception.JException("D3137", -1,
1840+
message if message is not None else "$error() function evaluated")
18401841

18411842
#
18421843
#
@@ -1851,8 +1852,8 @@ def assert_fn(condition: Optional[bool], message: Optional[str]) -> None:
18511852
raise jexception.JException("T0410", -1)
18521853

18531854
if not condition:
1854-
raise jexception.JException("D3141", -1, "$assert() statement failed")
1855-
# message: message || "$assert() statement failed"
1855+
raise jexception.JException("D3141", -1,
1856+
message if message is not None else "$assert() statement failed")
18561857

18571858
#
18581859
#

src/jsonata/jexception.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ def msg(error: str, location: int, arg1: Optional[Any], arg2: Optional[Any], det
105105

106106
formatted = message
107107

108+
if formatted == "{{{message}}}":
109+
return str(arg1)
110+
108111
# Replace any {{var}} with format "{}"
109112
formatted = re.sub("\\{\\{\\w+\\}\\}", "{}", formatted)
110113

src/jsonata/jsonata.py

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -469,7 +469,7 @@ def evaluate_tuple_step(self, expr: parser.Parser.Symbol, input: Optional[Sequen
469469
result.tuple_stream = True
470470
step_env = environment
471471
if tuple_bindings is None:
472-
tuple_bindings = [{"@": item} for item in input if item is not None]
472+
tuple_bindings = [{"@": item} for item in input]
473473

474474
for tuple_binding in tuple_bindings:
475475
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
519519
if index < 0:
520520
# count in from end of array
521521
index = len(input) + index
522-
item = input[index] if index < len(input) else None
522+
item = input[index] if 0 <= index < len(input) else None
523523
if item is not None:
524524
if isinstance(item, list):
525525
results = item
@@ -684,9 +684,6 @@ def evaluate_wildcard(self, expr: Optional[parser.Parser.Symbol], input: Optiona
684684
if isinstance(value, list):
685685
value = self.flatten(value, None)
686686
results = functions.Functions.append(results, value)
687-
elif isinstance(value, dict):
688-
# Call recursively do decompose the map
689-
results.extend(self.evaluate_wildcard(expr, value))
690687
else:
691688
results.append(value)
692689

@@ -1475,7 +1472,14 @@ def apply_inner(self, proc: Optional[Any], args: Optional[Any], input: Optional[
14751472
elif isinstance(proc, Jsonata.JLambda):
14761473
result = proc.call(input, validated_args)
14771474
elif isinstance(proc, re.Pattern):
1478-
result = [s for s in validated_args if proc.search(s) is not None]
1475+
_res = []
1476+
for s in validated_args:
1477+
if isinstance(s, str):
1478+
_res.append(Jsonata._regex_closure(proc.finditer(s)))
1479+
if len(_res) == 1:
1480+
result = _res[0]
1481+
else:
1482+
result = _res
14791483
else:
14801484
print("Proc not found " + str(proc))
14811485
raise jexception.JException("T1006", 0)
@@ -1489,6 +1493,19 @@ def apply_inner(self, proc: Optional[Any], args: Optional[Any], input: Optional[
14891493
raise err
14901494
return result
14911495

1496+
@staticmethod
1497+
def _regex_closure(iterator):
1498+
m = next(iterator, None)
1499+
if m is None:
1500+
return None
1501+
return {
1502+
"match": m.group(),
1503+
"start": m.start(),
1504+
"end": m.end(),
1505+
"groups": [m.group()],
1506+
"next": Jsonata.JLambda(lambda: Jsonata._regex_closure(iterator))
1507+
}
1508+
14921509
#
14931510
# Evaluate lambda against input data
14941511
# @param {Object} expr - JSONata expression

src/jsonata/parser.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1047,7 +1047,8 @@ def process_ast(self, expr: Optional[Symbol]) -> Optional[Symbol]:
10471047
rest = self.process_ast(expr.rhs)
10481048
if (rest.type == "function" and rest.procedure.type == "path" and len(
10491049
rest.procedure.steps) == 1 and rest.procedure.steps[0].type == "name" and
1050-
result.steps[-1].type == "function"):
1050+
result.steps[-1].type == "function" and
1051+
isinstance(rest.procedure.steps[0].value, Parser.Symbol)):
10511052
# next function in chain of functions - will override a thenable
10521053
result.steps[-1].next_function = rest.procedure.steps[0].value
10531054
if rest.type == "path":

src/jsonata/signature.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -308,6 +308,7 @@ def validate(self, args: Any, context: Optional[Any]) -> Optional[Any]:
308308
arg = args[arg_index] if arg_index < len(args) else None
309309
validated_args.append(arg)
310310
arg_index += 1
311+
index += 1
311312
return validated_args
312313
self.throw_validation_error(args, supplied_sig, self.function_name)
313314

src/jsonata/tokenizer.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -137,7 +137,10 @@ def scan_regex(self) -> re.Pattern:
137137
if pattern == "":
138138
raise jexception.JException("S0301", self.position)
139139
self.position += 1
140-
current_char = self.path[self.position]
140+
if self.position < self.length:
141+
current_char = self.path[self.position]
142+
else:
143+
current_char = None
141144
# flags
142145
start = self.position
143146
while current_char == 'i' or current_char == 'm':

src/jsonata/utils.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,7 @@ def is_function(o: Optional[Any]) -> bool:
7474
@staticmethod
7575
def create_sequence(el: Optional[Any] = NONE) -> list:
7676
if el is not Utils.NONE:
77-
if isinstance(el, list) and len(el) == 1:
78-
sequence = Utils.JList(el)
79-
else:
80-
# This case does NOT exist in Javascript! Why?
81-
sequence = Utils.JList([el])
77+
sequence = Utils.JList([el])
8278
else:
8379
sequence = Utils.JList()
8480
sequence.sequence = True

tests/array_test.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,40 @@
11
import jsonata
2+
import pytest
23

34

45
class TestArray:
56

7+
def test_negative_index(self):
8+
expr = jsonata.Jsonata("item[-1]")
9+
assert expr.evaluate({"item": []}) is None
10+
expr = jsonata.Jsonata("$[-1]")
11+
assert expr.evaluate([]) is None
12+
613
def test_array(self):
714
assert jsonata.Jsonata("$.[{ }] ~> $reduce($append)").evaluate([True, True]) == [{}, {}]
15+
16+
def test_wildcard(self):
17+
expr = jsonata.Jsonata("*")
18+
assert expr.evaluate([{"x": 1}]) == {"x": 1}
19+
20+
def test_index(self):
21+
expr = jsonata.Jsonata("($x:=['a','b']; $x#$i.$i)")
22+
assert expr.evaluate(1) == [0, 1]
23+
assert expr.evaluate(None) == [0, 1]
24+
25+
def test_wildcard_filter(self):
26+
value1 = {"value": {"Name": "Cell1", "Product": "Product1"}}
27+
value2 = {"value": {"Name": "Cell2", "Product": "Product2"}}
28+
data = [value1, value2]
29+
30+
expression = jsonata.Jsonata("*[value.Product = 'Product1']")
31+
assert expression.evaluate(data) == value1
32+
33+
expression2 = jsonata.Jsonata("**[value.Product = 'Product1']")
34+
assert expression2.evaluate(data) == value1
35+
36+
def test_assert_custom_message(self):
37+
expr = jsonata.Jsonata("$assert(false, 'custom error')")
38+
with pytest.raises(jsonata.JException) as exc_info:
39+
expr.evaluate(None)
40+
assert "custom error" in str(exc_info.value)

tests/signature_test.py

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ class TestSignature:
77
def test_parameters_are_converted_to_arrays(self):
88
expr = jsonata.Jsonata("$greet(1,null,3)")
99
expr.register_function("greet", jsonata.Jsonata.JFunction(TestSignature.JFunctionCallable1(), "<a?a?a?a?:s>"))
10-
assert expr.evaluate(None) == "[[1], [null], [3], [None]]"
10+
assert expr.evaluate(None) == "[[1], None, [3], None]"
1111

1212
class JFunctionCallable1(jsonata.Jsonata.JFunctionCallable):
1313

@@ -30,3 +30,13 @@ class JFunctionCallable2(jsonata.Jsonata.JFunctionCallable):
3030

3131
def call(self, input, args):
3232
return None
33+
34+
def test_var_arg_many(self):
35+
expr = jsonata.Jsonata("$customArgs('test',[1,2,3,4],3)")
36+
expr.register_function("customArgs", jsonata.Jsonata.JFunction(TestSignature.JFunctionCallable3(), "<sa<n>n:s>"))
37+
assert expr.evaluate(None) == "['test', [1, 2, 3, 4], 3]"
38+
39+
class JFunctionCallable3(jsonata.Jsonata.JFunctionCallable):
40+
41+
def call(self, input, args):
42+
return str(args)

0 commit comments

Comments
 (0)