From ab7e60b2ef29fc2fb75c4f4d6132d45b21dd88e5 Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Wed, 4 Nov 2026 14:00:34 +0100 Subject: [PATCH 1/5] Align partial function application with jsonata-js #95 Partially applying a native function now wraps the function itself as the lambda body (bodyAST.body = native) instead of generating a trampoline expression that calls the builtin by name. As in JS, the signature of the native function is not validated when the partial is invoked. - implement applyNativeFunction / getNativeFunctionArguments - only '?' leaves a parameter unbound, missing args are bound to null (#86) - fix '?' comparison (token value is a Character) - no lookup by name, so unnamed / custom JFunctions work (#83) - arity of a JFunctionCallable is taken from its signature - $each / $merge iterate arrays like JS for..in (#79) - fix inverted T1007 check Co-Authored-By: Claude Opus 5.5 --- .../java/com/dashjoin/jsonata/Functions.java | 26 +++++- .../java/com/dashjoin/jsonata/Jsonata.java | 86 +++++++++---------- .../java/com/dashjoin/jsonata/Parser.java | 4 +- .../com/dashjoin/jsonata/PartialTest.java | 75 ++++++++++++++++ 4 files changed, 138 insertions(+), 53 deletions(-) create mode 100644 src/test/java/com/dashjoin/jsonata/PartialTest.java diff --git a/src/main/java/com/dashjoin/jsonata/Functions.java b/src/main/java/com/dashjoin/jsonata/Functions.java index 4619d53..a717b36 100644 --- a/src/main/java/com/dashjoin/jsonata/Functions.java +++ b/src/main/java/com/dashjoin/jsonata/Functions.java @@ -1831,7 +1831,7 @@ public static Object merge(List arg) { var result = new LinkedHashMap<>(); for (Object obj : arg) { - for (Entry entry : ((Map)obj).entrySet()) { + for (Entry entry : forIn(obj).entrySet()) { result.put(entry.getKey(), entry.getValue()); } } @@ -1858,6 +1858,23 @@ public static List reverse(List arr) { return result; } + /** + * Java: emulates JavaScript "for (var key in obj)", which also iterates arrays + * (keys are the indexes as strings). Needed where the signature is not validated, + * e.g. for partially applied native functions like $each(?, ...) + */ + static Map forIn(Object obj) { + if (obj instanceof List) { + var res = new LinkedHashMap(); + for (int i=0; i<((List)obj).size(); i++) + res.put(""+i, ((List)obj).get(i)); + return res; + } + if (obj instanceof Map) + return (Map)obj; + return Map.of(); + } + /** * * @param {*} obj - the input object to iterate over @@ -1865,15 +1882,16 @@ public static List reverse(List arr) { * @throws Throwable * @returns {Array} - the resultant array */ - public static List each(Map obj, Object func) throws Throwable { - if (obj==null) { + public static List each(Object _obj, Object func) throws Throwable { + if (_obj==null) { return null; } + var obj = forIn(_obj); var result = createSequence(); for (var key : obj.keySet()) { - var func_args = hofFuncArgs(func, obj.get(key), key, obj); + var func_args = hofFuncArgs(func, obj.get(key), key, _obj); // invoke func var val = funcApply(func, func_args); if(val != null) { diff --git a/src/main/java/com/dashjoin/jsonata/Jsonata.java b/src/main/java/com/dashjoin/jsonata/Jsonata.java index 15c12d9..0b9a1b3 100644 --- a/src/main/java/com/dashjoin/jsonata/Jsonata.java +++ b/src/main/java/com/dashjoin/jsonata/Jsonata.java @@ -1742,16 +1742,16 @@ Jsonata getPerThreadInstance() { // trampoline loop - this gets invoked as a result of tail-call optimization // the Object returned a tail-call thunk // unpack it, evaluate its arguments, and apply the tail call - var next = /* await */ evaluate(((Symbol)result).body.procedure, ((Symbol)result).input, ((Symbol)result).environment); - if(((Symbol)result).body.procedure.type == "variable") { + var next = /* await */ evaluate(((Symbol)((Symbol)result).body).procedure, ((Symbol)result).input, ((Symbol)result).environment); + if(((Symbol)((Symbol)result).body).procedure.type == "variable") { if (next instanceof Symbol) // Java: not if JFunction - ((Symbol)next).token = ((Symbol)result).body.procedure.value; + ((Symbol)next).token = ((Symbol)((Symbol)result).body).procedure.value; } if (next instanceof Symbol) // Java: not if JFunction - ((Symbol)next).position = ((Symbol)result).body.procedure.position; + ((Symbol)next).position = ((Symbol)((Symbol)result).body).procedure.position; var evaluatedArgs = new ArrayList<>(); - for(var ii = 0; ii < ((Symbol)result).body.arguments.size(); ii++) { - evaluatedArgs.add(/* await */ evaluate(((Symbol)result).body.arguments.get(ii), ((Symbol)result).input, ((Symbol)result).environment)); + for(var ii = 0; ii < ((Symbol)((Symbol)result).body).arguments.size(); ii++) { + evaluatedArgs.add(/* await */ evaluate(((Symbol)((Symbol)result).body).arguments.get(ii), ((Symbol)result).input, ((Symbol)result).environment)); } result = /* await */ applyInner(next, evaluatedArgs, input, environment); @@ -1908,7 +1908,7 @@ Object evaluateLambda(Symbol expr, Object input, Frame environment) { var evaluatedArgs = new ArrayList<>(); for(var ii = 0; ii < expr.arguments.size(); ii++) { var arg = expr.arguments.get(ii); - if (arg.type.equals("operator") && (arg.value.equals("?"))) { + if (arg.type.equals("operator") && (""+arg.value).equals("?")) { evaluatedArgs.add(arg); } else { evaluatedArgs.add(/* await */ evaluate(arg, input, environment)); @@ -1916,7 +1916,7 @@ Object evaluateLambda(Symbol expr, Object input, Frame environment) { } // lookup the procedure var proc = /* await */ evaluate(expr.procedure, input, environment); - if (proc != null && expr.procedure.type.equals("path") && environment.lookup((String)expr.procedure.steps.get(0).value)!=null) { + if (proc == null && expr.procedure.type.equals("path") && environment.lookup((String)expr.procedure.steps.get(0).value)!=null) { // help the user out here if they simply forgot the leading $ throw new JException("T1007", expr.position, @@ -1973,12 +1973,12 @@ Object validateArguments(Object signature, Object args, Object context) { if (i>=args.size()) break; env.bind(""+proc.arguments.get(i).value, args.get(i)); } - if (proc.body instanceof Symbol) { - result = evaluate(proc.body, proc.input, env); + if (proc.body instanceof JFunction) { + // this is a lambda that wraps a native Object - generated by partially evaluating a native + result = /* await */ applyNativeFunction((JFunction)proc.body, env); + } else if (proc.body instanceof Symbol) { + result = evaluate((Symbol)proc.body, proc.input, env); } else throw new Error("Cannot execute procedure: "+proc+" "+proc.body); - // if (typeof proc.body === "function") { - // // this is a lambda that wraps a native Object - generated by partially evaluating a native - // result = /* await */ applyNativeFunction(proc.body, env); return result; } @@ -1997,7 +1997,7 @@ Object partialApplyProcedure(Symbol proc, List args) { for (var param : proc.arguments) { // proc.arguments.forEach(Object (param, index) { Object arg = index(); - List partArgs = new ArrayList<>(); - for (int i=0; i<_native.getNumberOfArgs(); i++) { - String argName = "$" + (char)('a'+i); - sigArgs.add(argName); - if (i>=args.size() || args.get(i)==null) - partArgs.add(argName); - else - partArgs.add(args.get(i)); - } - - var body = "function(" + String.join(", ", sigArgs) + "){"; - body += "$"+_native.functionName+"("+String.join(", ", sigArgs) + ") }"; - - if (log.isLoggable(Level.FINE)) log.fine("partial trampoline = "+body); - - // var sigArgs = getNativeFunctionArguments(_native); - // sigArgs = sigArgs.stream().map(sigArg -> { - // return "$" + sigArg; - // }).toList(); - // var body = "function(" + String.join(", ", sigArgs) + "){ _ }"; + var sigArgs = getNativeFunctionArguments(_native); + sigArgs = sigArgs.stream().map(sigArg -> { + return "$" + sigArg; + }).toList(); + var body = "function(" + String.join(", ", sigArgs) + "){ _ }"; var bodyAST = parser.parse(body); - //bodyAST.body = _native; + bodyAST.body = _native; var partial = partialApplyProcedure(bodyAST, (List)args); return partial; } - + /** * Apply native function * @param {Object} proc - Procedure @@ -2064,8 +2044,14 @@ Object partialApplyNativeFunction(JFunction _native, List args) { * @returns {*} Result of applying native function */ /* async */ Object applyNativeFunction(JFunction proc, Frame env) { - // Not called in Java - JFunction call directly calls native function - return null; + var sigArgs = getNativeFunctionArguments(proc); + // generate the array of arguments for invoking the function - look them up in the environment + var args = sigArgs.stream().map(sigArg -> { + return env.lookup(sigArg); + }).toList(); + + var result = proc.call(null, args); + return result; } /** @@ -2073,9 +2059,12 @@ Object partialApplyNativeFunction(JFunction _native, List args) { * @param {Function} func - Function * @returns {*|Array} Native Object arguments */ - List getNativeFunctionArguments(JFunction func) { - // Not called in Java - return null; + List getNativeFunctionArguments(JFunction func) { + // Java: parameter names are not available, use a, b, c, ... + var sigArgs = new ArrayList(); + for (int i=0; i arguments; - Symbol body; + Object body; // Symbol, or JFunction for a partially applied native function List predicate; public List stages; public Object input; @@ -1291,7 +1291,7 @@ Symbol processAST(Symbol expr) { result.arguments = expr.arguments; result.signature = expr.signature; result.position = expr.position; - var body = processAST(expr.body); + var body = processAST((Symbol)expr.body); result.body = tailCallOptimize(body); break; case "condition": diff --git a/src/test/java/com/dashjoin/jsonata/PartialTest.java b/src/test/java/com/dashjoin/jsonata/PartialTest.java new file mode 100644 index 0000000..afc894e --- /dev/null +++ b/src/test/java/com/dashjoin/jsonata/PartialTest.java @@ -0,0 +1,75 @@ +package com.dashjoin.jsonata; + +import static com.dashjoin.jsonata.Jsonata.jsonata; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import com.dashjoin.jsonata.Jsonata.JFunction; +import com.dashjoin.jsonata.Jsonata.JFunctionCallable; + +/** + * Partial function application, see issue #95 + */ +public class PartialTest { + + // #79 + @Test + public void eachEmptyArray() { + Assertions.assertNull(jsonata("[] ~> $each(?, function($v) { $v })").evaluate(Map.of())); + } + + // #86 + @Test + public void mapAppend() { + Assertions.assertEquals(Map.of("systems", "asd2"), + jsonata("{'systems': 'asd2'}.$map($, $append(?))").evaluate(null)); + } + + // missing args are bound to undefined, not left unbound + @Test + public void missingArgsBound() { + Assertions.assertEquals("ello", jsonata("$substring(?, 1)('hello')").evaluate(null)); + } + + // #83 + @Test + public void customFunction() { + var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); + expression.registerFunction("add", new JFunction(new JFunctionCallable() { + @SuppressWarnings("rawtypes") + @Override + public Object call(Object input, List args) throws Throwable { + return (Integer) args.get(0) + (Integer) args.get(1); + } + }, "")); + Assertions.assertEquals(6, expression.evaluate(null)); + } + + // JS iterates array indexes, signature is not checked for partially applied natives + @Test + public void eachArray() { + Assertions.assertEquals(List.of("0", "1"), + jsonata("[5,6] ~> $each(?, function($v,$k) { $k })").evaluate(null)); + } + + @Test + public void mergeWithArray() { + Assertions.assertEquals(Map.of("test", "data"), + jsonata("$ ~> $merge(?)").evaluate(List.of(List.of(), Map.of("test", "data")))); + } + + // signature still checked when called directly + @Test + public void eachArrayDirect() { + Assertions.assertThrows(JException.class, + () -> jsonata("$each([5,6], function($v,$k) { $k })").evaluate(null)); + } + + // forgot the leading $ + @Test + public void missingDollar() { + var ex = Assertions.assertThrows(JException.class, () -> jsonata("substring(?, 1)").evaluate(null)); + Assertions.assertEquals("T1007", ex.getError()); + } +} From cb16ba0282e3bbca6c167afdd895c1b667f399fe Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Wed, 4 Nov 2026 14:09:10 +0100 Subject: [PATCH 2/5] Use Collectors.toList() for JDK 11 compatibility Co-Authored-By: Claude Opus 5.5 --- src/main/java/com/dashjoin/jsonata/Jsonata.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/dashjoin/jsonata/Jsonata.java b/src/main/java/com/dashjoin/jsonata/Jsonata.java index 0b9a1b3..ccff12d 100644 --- a/src/main/java/com/dashjoin/jsonata/Jsonata.java +++ b/src/main/java/com/dashjoin/jsonata/Jsonata.java @@ -2027,7 +2027,7 @@ Object partialApplyNativeFunction(JFunction _native, List args) { var sigArgs = getNativeFunctionArguments(_native); sigArgs = sigArgs.stream().map(sigArg -> { return "$" + sigArg; - }).toList(); + }).collect(Collectors.toList()); var body = "function(" + String.join(", ", sigArgs) + "){ _ }"; var bodyAST = parser.parse(body); @@ -2048,7 +2048,7 @@ Object partialApplyNativeFunction(JFunction _native, List args) { // generate the array of arguments for invoking the function - look them up in the environment var args = sigArgs.stream().map(sigArg -> { return env.lookup(sigArg); - }).toList(); + }).collect(Collectors.toList()); var result = proc.call(null, args); return result; From ef116f64ab9997c93e8662dbdb5de4e2ea0ec6b2 Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Wed, 4 Nov 2026 14:23:37 +0100 Subject: [PATCH 3/5] Partial application of callables and lambdas without declared arity #95 - JLambda (Fn0..Fn8) reports its number of declared parameters, which JFunctions created via Jsonata.function(...) keep - arity unknown (callable / FnVarArgs without signature): use the number of args supplied at the partial application - implement the JS "typeof proc === 'function'" branch: lambdas bound via Frame.bind can be partially applied Co-Authored-By: Claude Opus 5.5 --- .../java/com/dashjoin/jsonata/Jsonata.java | 60 +++++++++++++++---- .../com/dashjoin/jsonata/PartialTest.java | 33 ++++++++++ 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/src/main/java/com/dashjoin/jsonata/Jsonata.java b/src/main/java/com/dashjoin/jsonata/Jsonata.java index ccff12d..ec7a6cd 100644 --- a/src/main/java/com/dashjoin/jsonata/Jsonata.java +++ b/src/main/java/com/dashjoin/jsonata/Jsonata.java @@ -1927,8 +1927,8 @@ Object evaluateLambda(Symbol expr, Object input, Frame environment) { result = partialApplyProcedure((Symbol)proc, (List)evaluatedArgs); } else if (Utils.isFunction(proc)) { result = partialApplyNativeFunction((JFunction)proc /*.implementation*/, evaluatedArgs); - // } else if (typeof proc === "function") { - // result = partialApplyNativeFunction(proc, evaluatedArgs); + } else if (proc instanceof JLambda) { + result = partialApplyNativeFunction(new JFunction((JLambda)proc, null), evaluatedArgs); } else { throw new JException("T1008", //stack: (new Error()).stack, @@ -2024,6 +2024,12 @@ Object partialApplyNativeFunction(JFunction _native, List args) { // create a lambda Object that wraps and invokes the native function // get the list of declared arguments from the native function // this has to be picked out from the toString() value + if (_native.getNumberOfArgs() < 0) { + // Java: parameters unknown (callable without signature), use the number of args supplied + var fn = new JFunction(_native.function, null); + fn.numberOfArgs = args.size(); + _native = fn; + } var sigArgs = getNativeFunctionArguments(_native); sigArgs = sigArgs.stream().map(sigArg -> { return "$" + sigArg; @@ -2087,34 +2093,34 @@ public static JFunction function(String name, String signature, Class clazz, Obj } public static JFunction function(String name, FnVarArgs func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn0 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn1 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn2 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn3 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn4 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn5 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn6 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn7 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } public static JFunction function(String name, Fn8 func, String signature) { - return new JFunction(func.getJFunctionCallable(), signature); + return new JFunction(func, signature); } /** @@ -2170,6 +2176,14 @@ public Frame createFrame(Frame enclosingEnvironment) { } public static interface JLambda { + JFunctionCallable getJFunctionCallable(); + + /** + * @return the number of declared parameters, -1 if unknown (varargs) + */ + default int getNumberOfArgs() { + return -1; + } } public static interface FnVarArgs extends JLambda, Function, R> { @@ -2180,21 +2194,25 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn0 extends JLambda, Supplier { + default int getNumberOfArgs() { return 0; } default JFunctionCallable getJFunctionCallable() { return (input, args) -> get(); } } public static interface Fn1 extends JLambda, Function { + default int getNumberOfArgs() { return 1; } default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0)); } } public static interface Fn2 extends JLambda, BiFunction { + default int getNumberOfArgs() { return 2; } default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1)); } } public static interface Fn3 extends JLambda { + default int getNumberOfArgs() { return 3; } R apply(A a, B b, C c); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2202,6 +2220,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn4 extends JLambda { + default int getNumberOfArgs() { return 4; } R apply(A a, B b, C c, D d); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2209,6 +2228,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn5 extends JLambda { + default int getNumberOfArgs() { return 5; } R apply(A a, B b, C c, D d, E e); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2216,6 +2236,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn6 extends JLambda { + default int getNumberOfArgs() { return 6; } R apply(A a, B b, C c, D d, E e, F f); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2224,6 +2245,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn7 extends JLambda { + default int getNumberOfArgs() { return 7; } R apply(A a, B b, C c, D d, E e, F f, G g); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2232,6 +2254,7 @@ default JFunctionCallable getJFunctionCallable() { } } public static interface Fn8 extends JLambda { + default int getNumberOfArgs() { return 8; } R apply(A a, B b, C c, D d, E e, F f, G g, H h); default JFunctionCallable getJFunctionCallable() { return (input, args) -> apply((A) args.get(0), (B) args.get(1), @@ -2260,6 +2283,12 @@ public static class JFunction implements JFunctionCallable, JFunctionSignatureVa Signature signature; Method method; Object methodInstance; + int numberOfArgs = -1; + + public JFunction(JLambda lambda, String signature) { + this(lambda.getJFunctionCallable(), signature); + this.numberOfArgs = lambda.getNumberOfArgs(); + } public JFunction(JFunctionCallable function, String signature) { this.function = function; @@ -2306,11 +2335,16 @@ public Object validate(Object args, Object context) { return args; } + /** + * @return the number of declared parameters, -1 if unknown + */ public int getNumberOfArgs() { if (method != null) return method.getParameterTypes().length; + if (numberOfArgs >= 0) + return numberOfArgs; // Java: a JFunctionCallable has no declared parameters, use the signature (if any) - return signature != null ? signature.getNumberOfArgs() : 0; + return signature != null ? signature.getNumberOfArgs() : -1; } } diff --git a/src/test/java/com/dashjoin/jsonata/PartialTest.java b/src/test/java/com/dashjoin/jsonata/PartialTest.java index afc894e..e22be0e 100644 --- a/src/test/java/com/dashjoin/jsonata/PartialTest.java +++ b/src/test/java/com/dashjoin/jsonata/PartialTest.java @@ -6,6 +6,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import com.dashjoin.jsonata.Jsonata.JFunction; +import com.dashjoin.jsonata.Jsonata.Fn2; import com.dashjoin.jsonata.Jsonata.JFunctionCallable; /** @@ -72,4 +73,36 @@ public void missingDollar() { var ex = Assertions.assertThrows(JException.class, () -> jsonata("substring(?, 1)").evaluate(null)); Assertions.assertEquals("T1007", ex.getError()); } + + // Known gap: a JFunctionCallable without signature has no arity (#83) + @Test + public void customFunctionWithoutSignature() { + var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); + expression.registerFunction("add", new JFunction(new JFunctionCallable() { + @SuppressWarnings("rawtypes") + @Override + public Object call(Object input, List args) throws Throwable { + return (Integer) args.get(0) + (Integer) args.get(1); + } + }, null)); + Assertions.assertEquals(6, expression.evaluate(null)); + } + + // Known gap: same for the Fn* factories when no signature is given + @Test + public void fnFactoryWithoutSignature() { + var expression = jsonata("$add(1, 2) ~> $add(?, 3)"); + expression.registerFunction("add", + Jsonata.function("add", (Fn2) (a, b) -> a + b, null)); + Assertions.assertEquals(6, expression.evaluate(null)); + } + + // Known gap: Fn* lambdas bound via Frame.bind cannot be partially applied (T1008) + @Test + public void boundLambda() { + var expression = jsonata("$f(1, 2) ~> $f(?, 3)"); + var frame = expression.createFrame(); + frame.bind("f", (Fn2) (a, b) -> a + b); + Assertions.assertEquals(6, expression.evaluate(null, frame)); + } } From dd89e918b795014bcbcc758061fdb193bb65df78 Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Wed, 4 Nov 2026 14:40:13 +0100 Subject: [PATCH 4/5] Add test cases from #80, #85, #87 Tests contributed by @sergeevik in the earlier PRs, adapted to the existing API. All pass with the partial application changes. Co-Authored-By: Claude Opus 5.5 --- .../java/com/dashjoin/jsonata/OldPrsTest.java | 87 +++++++++++++++++++ src/test/resources/map_in_pipe/answer.json | 30 +++++++ src/test/resources/map_in_pipe/input.json | 58 +++++++++++++ 3 files changed, 175 insertions(+) create mode 100644 src/test/java/com/dashjoin/jsonata/OldPrsTest.java create mode 100644 src/test/resources/map_in_pipe/answer.json create mode 100644 src/test/resources/map_in_pipe/input.json diff --git a/src/test/java/com/dashjoin/jsonata/OldPrsTest.java b/src/test/java/com/dashjoin/jsonata/OldPrsTest.java new file mode 100644 index 0000000..d77d9d6 --- /dev/null +++ b/src/test/java/com/dashjoin/jsonata/OldPrsTest.java @@ -0,0 +1,87 @@ +package com.dashjoin.jsonata; + +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.List; +import java.util.Map; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import com.dashjoin.jsonata.Jsonata.JFunction; +import com.dashjoin.jsonata.json.Json; + +/** tests from PRs #80, #85, #87, adapted to the existing API */ +public class OldPrsTest { + + static Object res(String name) throws Exception { + return Json.parseJson(new InputStreamReader( + OldPrsTest.class.getClassLoader().getResourceAsStream(name), StandardCharsets.UTF_8)); + } + + // #80 + @Test + public void testEachEmptyArray() { + Assertions.assertNull(Jsonata.jsonata("[] ~> $each(?, function($v) { $v })").evaluate("{}")); + } + + @Test + public void testEachArrayWithData() { + Assertions.assertEquals(List.of(123, 321), + Jsonata.jsonata("[123, 321] ~> $each(?, function($v) { $v })").evaluate("{}")); + } + + @Test + public void testMapInPipe() throws Exception { + var expression = Jsonata.jsonata("$sift($.buildings, function($v, $k) { $k ~> /o/ and $v.presentation.*.widget})" + + " ~> $each(?, function($v, $key) {{ $key: $sift($v.presentation, function($v, $k){ $v.widget }) }})" + + " ~> $map(?, function($v){($v.*)})"); + Assertions.assertEquals(res("map_in_pipe/answer.json"), expression.evaluate(res("map_in_pipe/input.json"))); + } + + // #85 (PR adds a name / arity constructor, here: plain constructor) + @SuppressWarnings("unchecked") + @Test + public void testFunctionWithInputData() { + JFunction addWithFactor = new JFunction((input, args) -> { + int factor = Integer.parseInt(((Map) input).get("factor").toString()); + return ((Integer) args.get(0) + (Integer) args.get(1)) * factor; + }, null); + var expression = Jsonata.jsonata("$addWithFactor(1, 2)"); + expression.registerFunction("addWithFactor", addWithFactor); + Assertions.assertEquals(9, expression.evaluate(Map.of("factor", "3"))); + } + + @SuppressWarnings("unchecked") + @Test + public void testFunctionWithInputData2() { + JFunction multiply = new JFunction((input, args) -> { + Object first = args.get(0); + Object second = args.size() > 1 ? args.get(1) + : Integer.parseInt(((Map) input).get("factor").toString()); + return (Integer) first * (Integer) second; + }, null); + var expression = Jsonata.jsonata("$multiply(3) ~> $multiply(?, 2)"); + expression.registerFunction("multiply", multiply); + Assertions.assertEquals(18, expression.evaluate(Map.of("factor", "3"))); + } + + @Test + public void testCustomFunctionPipe() { + JFunction add = new JFunction((input, args) -> (Integer) args.get(0) + (Integer) args.get(1), null); + var expression = Jsonata.jsonata("$add(1, 2) ~> $add(?, 3)"); + expression.registerFunction("add", add); + Assertions.assertEquals(6, expression.evaluate(null)); + } + + // #87 + @Test + public void testMapWithAppend() { + Assertions.assertEquals(Json.parseJson("{\"systems\": \"asd\"}"), + Jsonata.jsonata("{'systems': 'asd'}.$map($, $append(?))").evaluate(null)); + } + + @Test + public void testMapWithAppend2() { + Object answer = Json.parseJson("{\"systems\": \"asd\"}"); + Assertions.assertEquals(answer, Jsonata.jsonata("$map($, $append(?))").evaluate(answer)); + } +} diff --git a/src/test/resources/map_in_pipe/answer.json b/src/test/resources/map_in_pipe/answer.json new file mode 100644 index 0000000..5c5e15a --- /dev/null +++ b/src/test/resources/map_in_pipe/answer.json @@ -0,0 +1,30 @@ +[ + { + "first": { + "widget": { + "data": "home widget" + } + } + }, + { + "first": { + "widget": { + "data": "work widget" + } + } + }, + { + "first": { + "widget": { + "data": "old widget" + } + } + }, + { + "first": { + "widget": { + "data": "operation widget" + } + } + } +] \ No newline at end of file diff --git a/src/test/resources/map_in_pipe/input.json b/src/test/resources/map_in_pipe/input.json new file mode 100644 index 0000000..5359645 --- /dev/null +++ b/src/test/resources/map_in_pipe/input.json @@ -0,0 +1,58 @@ +{ + "buildings": { + "home": { + "address": "home address", + "period": "11 month on 12", + "presentation": { + "first": { + "widget": { + "data": "home widget" + } + } + } + }, + "work": { + "address": "work address", + "presentation": { + "first": { + "widget": { + "data": "work widget" + } + } + } + }, + "vacancy": { + "address": "vacancy address", + "period": "1 month on 12", + "presentation": { + "first": { + "widget": { + "data": "vacancy widget" + } + } + } + }, + "old": { + "address": "old address", + "period": "2 month on 12", + "presentation": { + "first": { + "widget": { + "data": "old widget" + } + } + } + }, + "operation": { + "address": "operation address", + "period": "3 month on 12", + "presentation": { + "first": { + "widget": { + "data": "operation widget" + } + } + } + } + } +} \ No newline at end of file From 64ec3c7d2f89065788425762a9fe049ed59f091d Mon Sep 17 00:00:00 2001 From: Andreas Eberhart Date: Wed, 4 Nov 2026 15:03:03 +0100 Subject: [PATCH 5/5] Iterate arrays in $each / $merge without copying them (review) Co-Authored-By: Claude Opus 5.5 --- .../java/com/dashjoin/jsonata/Functions.java | 59 ++++++++++--------- 1 file changed, 31 insertions(+), 28 deletions(-) diff --git a/src/main/java/com/dashjoin/jsonata/Functions.java b/src/main/java/com/dashjoin/jsonata/Functions.java index a717b36..aa82aa1 100644 --- a/src/main/java/com/dashjoin/jsonata/Functions.java +++ b/src/main/java/com/dashjoin/jsonata/Functions.java @@ -1831,8 +1831,15 @@ public static Object merge(List arg) { var result = new LinkedHashMap<>(); for (Object obj : arg) { - for (Entry entry : forIn(obj).entrySet()) { - result.put(entry.getKey(), entry.getValue()); + if (obj instanceof List) { + // Java: JS "for (var prop in obj)" also iterates arrays (keys are the indexes as strings). + // Can happen where the signature is not validated, e.g. $merge(?) + for (int i=0; i<((List)obj).size(); i++) + result.put(""+i, ((List)obj).get(i)); + } else if (obj instanceof Map) { + for (Entry entry : ((Map)obj).entrySet()) { + result.put(entry.getKey(), entry.getValue()); + } } } return result; @@ -1858,23 +1865,6 @@ public static List reverse(List arr) { return result; } - /** - * Java: emulates JavaScript "for (var key in obj)", which also iterates arrays - * (keys are the indexes as strings). Needed where the signature is not validated, - * e.g. for partially applied native functions like $each(?, ...) - */ - static Map forIn(Object obj) { - if (obj instanceof List) { - var res = new LinkedHashMap(); - for (int i=0; i<((List)obj).size(); i++) - res.put(""+i, ((List)obj).get(i)); - return res; - } - if (obj instanceof Map) - return (Map)obj; - return Map.of(); - } - /** * * @param {*} obj - the input object to iterate over @@ -1882,20 +1872,33 @@ static Map forIn(Object obj) { * @throws Throwable * @returns {Array} - the resultant array */ - public static List each(Object _obj, Object func) throws Throwable { - if (_obj==null) { + public static List each(Object obj, Object func) throws Throwable { + if (obj==null) { return null; } - var obj = forIn(_obj); var result = createSequence(); - for (var key : obj.keySet()) { - var func_args = hofFuncArgs(func, obj.get(key), key, _obj); - // invoke func - var val = funcApply(func, func_args); - if(val != null) { - result.add(val); + if (obj instanceof List) { + // Java: JS "for (var key in obj)" also iterates arrays (keys are the indexes as strings). + // Can happen where the signature is not validated, e.g. $each(?, ...) + var list = (List)obj; + for (int i=0; i