Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ venv/
.idea
.venv
tests/gen

.claude
7 changes: 4 additions & 3 deletions src/jsonata/functions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

#
#
Expand All @@ -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")

#
#
Expand Down
3 changes: 3 additions & 0 deletions src/jsonata/jexception.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
29 changes: 23 additions & 6 deletions src/jsonata/jsonata.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion src/jsonata/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand Down
1 change: 1 addition & 0 deletions src/jsonata/signature.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
5 changes: 4 additions & 1 deletion src/jsonata/tokenizer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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':
Expand Down
6 changes: 1 addition & 5 deletions src/jsonata/utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
33 changes: 33 additions & 0 deletions tests/array_test.py
Original file line number Diff line number Diff line change
@@ -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)
12 changes: 11 additions & 1 deletion tests/signature_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(), "<a?a?a?a?:s>"))
assert expr.evaluate(None) == "[[1], [null], [3], [None]]"
assert expr.evaluate(None) == "[[1], None, [3], None]"

class JFunctionCallable1(jsonata.Jsonata.JFunctionCallable):

Expand All @@ -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(), "<sa<n>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)
32 changes: 32 additions & 0 deletions tests/string_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
#
Expand Down Expand Up @@ -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) == ""
Expand Down
Loading