diff --git a/src/main/java/com/dashjoin/jsonata/Functions.java b/src/main/java/com/dashjoin/jsonata/Functions.java index 4619d53..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 : ((Map)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; @@ -1865,19 +1872,33 @@ public static List reverse(List arr) { * @throws Throwable * @returns {Array} - the resultant array */ - public static List each(Map obj, Object func) throws Throwable { + public static List each(Object obj, Object func) throws Throwable { if (obj==null) { return null; } 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(); - 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, @@ -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, @@ -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) + "){ _ }"; + 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; + }).collect(Collectors.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 +2050,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); + }).collect(Collectors.toList()); + + var result = proc.call(null, args); + return result; } /** @@ -2073,9 +2065,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 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); } /** @@ -2181,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> { @@ -2191,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), @@ -2213,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), @@ -2220,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), @@ -2227,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), @@ -2235,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), @@ -2243,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), @@ -2271,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; @@ -2317,8 +2335,16 @@ public Object validate(Object args, Object context) { return args; } + /** + * @return the number of declared parameters, -1 if unknown + */ public int getNumberOfArgs() { - return method != null ? method.getParameterTypes().length : 0; + 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() : -1; } } diff --git a/src/main/java/com/dashjoin/jsonata/Parser.java b/src/main/java/com/dashjoin/jsonata/Parser.java index 9a3a4b6..60b1ebf 100644 --- a/src/main/java/com/dashjoin/jsonata/Parser.java +++ b/src/main/java/com/dashjoin/jsonata/Parser.java @@ -126,7 +126,7 @@ public class Symbol implements Cloneable, Serializable { // Procedure: Symbol procedure; List 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/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/java/com/dashjoin/jsonata/PartialTest.java b/src/test/java/com/dashjoin/jsonata/PartialTest.java new file mode 100644 index 0000000..e22be0e --- /dev/null +++ b/src/test/java/com/dashjoin/jsonata/PartialTest.java @@ -0,0 +1,108 @@ +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.Fn2; +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()); + } + + // 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)); + } +} 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