This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch fix/CAMEL-25470 in repository https://gitbox.apache.org/repos/asf/camel.git
commit 5488d9c291be3afd5d2fc49792fe1a5e7b3281b6 Author: Claus Ibsen <[email protected]> AuthorDate: Thu Oct 8 22:22:23 2026 +0200 CAMEL-25470: camel-dataweave - $ shorthand mis-converted with selectors, objects and function calls The $ lambda shorthand was only converted when $ was the whole lambda body. Anything else was emitted without a TODO, so fail-fast did not catch it: - $.@attr, $.*field, $.a? and $[0] lost the selector or bound it to the map/filter result (payload map $.@id became "function(x) x."). - A body with $ inside an object, function call or unary minus became a function per field (map {id: $.id}, map upper($.name), orderBy -$.price). - distinctBy $.id dropped the key and did a plain distinct. The parser now keeps the selectors on $, and the converter emits a shorthand body in a scope where $ refers to the item (and $$ to the reduce accumulator or the map index), so any expression works and nested shorthand lambdas get their own scope. This also replaces the reduce-only rewriting, which left if/else, default and function calls in a reduce shorthand as TODO. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/docs/datasonnet-language.adoc | 1 + .../src/main/docs/datasonnet-language.adoc | 1 + .../datasonnet/DataWeaveExecutedCorpusTest.java | 28 +++ .../src/test/resources/corpus/shorthand.dwl | 11 ++ .../component/dataweave/DataWeaveConverter.java | 190 ++++++++++----------- .../camel/component/dataweave/DataWeaveParser.java | 45 ++++- .../dataweave/DataWeaveConverterTest.java | 117 ++++++++++++- 7 files changed, 293 insertions(+), 100 deletions(-) diff --git a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/datasonnet-language.adoc b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/datasonnet-language.adoc index 539b17aa9027..b0549cdee850 100644 --- a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/datasonnet-language.adoc +++ b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/datasonnet-language.adoc @@ -519,6 +519,7 @@ The transpiler automatically maps DataWeave constructs to their DataSonnet equiv - `payload` to `body`, `vars` to `cml.variable()`, `attributes.headers` to `cml.header()` - Operators: `++` to `+`, `default` to `cml.defaultVal()`, `as Number/String/Boolean` to `cml.toInteger()/std.toString()/cml.toBoolean()` - Collection operations: `map`, `filter`, `reduce` (including `$$`/`$` shorthand form), `flatMap`, `groupBy`, `orderBy`, `distinctBy` +- The `$` shorthand in any of these operations, including in object constructors and function calls (`payload map { id: $.id, name: upper($.name) }`), with `$$` as the index in `map` and the accumulator in `reduce` - String operations: `contains`, `startsWith`, `endsWith`, `splitBy`, `joinBy`, `replace` - Functions: `sizeOf`, `upper`, `lower`, `trim`, `now`, `uuid`, `abs`, `round`, `sqrt` - XML selectors: multi-value selector `.*field` (maps all `field` values from a collection), attribute access `.@attr` (maps to `["@attr"]`) diff --git a/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc b/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc index 539b17aa9027..b0549cdee850 100644 --- a/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc +++ b/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc @@ -519,6 +519,7 @@ The transpiler automatically maps DataWeave constructs to their DataSonnet equiv - `payload` to `body`, `vars` to `cml.variable()`, `attributes.headers` to `cml.header()` - Operators: `++` to `+`, `default` to `cml.defaultVal()`, `as Number/String/Boolean` to `cml.toInteger()/std.toString()/cml.toBoolean()` - Collection operations: `map`, `filter`, `reduce` (including `$$`/`$` shorthand form), `flatMap`, `groupBy`, `orderBy`, `distinctBy` +- The `$` shorthand in any of these operations, including in object constructors and function calls (`payload map { id: $.id, name: upper($.name) }`), with `$$` as the index in `map` and the accumulator in `reduce` - String operations: `contains`, `startsWith`, `endsWith`, `splitBy`, `joinBy`, `replace` - Functions: `sizeOf`, `upper`, `lower`, `trim`, `now`, `uuid`, `abs`, `round`, `sqrt` - XML selectors: multi-value selector `.*field` (maps all `field` values from a collection), attribute access `.@attr` (maps to `["@attr"]`) diff --git a/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/DataWeaveExecutedCorpusTest.java b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/DataWeaveExecutedCorpusTest.java index 13f7c23b8041..3a464b196d12 100644 --- a/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/DataWeaveExecutedCorpusTest.java +++ b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/DataWeaveExecutedCorpusTest.java @@ -131,6 +131,29 @@ class DataWeaveExecutedCorpusTest extends CamelTestSupport { JSONAssert.assertEquals("{\"amount\":42,\"label\":\"42\"}", result, true); } + /** + * The $ / $$ shorthand inside object constructors, function calls, unary minus, attribute and existence selectors, + * where $$ is the index in map and the accumulator in reduce. + */ + @Test + void testShorthand() throws Exception { + template.sendBody("direct:shorthand", "{\"items\":[" + + "{\"@sku\":\"A1\",\"name\":\"apple\",\"price\":3,\"qty\":2}," + + "{\"@sku\":\"B2\",\"name\":\"pear\",\"price\":5,\"qty\":1,\"discount\":true}," + + "{\"@sku\":\"A1\",\"name\":\"apple\",\"price\":3,\"qty\":4}]}"); + MockEndpoint mock = getMockEndpoint("mock:shorthand"); + mock.assertExchangeReceived(0); + String result = mock.getExchanges().get(0).getMessage().getBody(String.class); + JSONAssert.assertEquals("{" + + "\"names\":[\"APPLE\",\"PEAR\",\"APPLE\"]," + + "\"lines\":[{\"line\":0,\"sku\":\"A1\"},{\"line\":1,\"sku\":\"B2\"},{\"line\":2,\"sku\":\"A1\"}]," + + "\"byPriceDesc\":[\"pear\",\"apple\",\"apple\"]," + + "\"discounted\":[\"pear\"]," + + "\"skus\":[\"A1\",\"B2\"]," + + "\"totalQty\":7}", + result, true); + } + // ------------------------------------------------------------------------- // Fail-fast test — unsupported construct must throw at expression creation time // ------------------------------------------------------------------------- @@ -215,6 +238,11 @@ class DataWeaveExecutedCorpusTest extends CamelTestSupport { .transform(datasonnet("resource:classpath:corpus/typeCoercion.dwl", String.class, MediaTypes.APPLICATION_JSON_VALUE, MediaTypes.APPLICATION_JSON_VALUE)) .to("mock:typeCoercion"); + + from("direct:shorthand") + .transform(datasonnet("resource:classpath:corpus/shorthand.dwl", String.class, + MediaTypes.APPLICATION_JSON_VALUE, MediaTypes.APPLICATION_JSON_VALUE)) + .to("mock:shorthand"); } }; } diff --git a/components/camel-datasonnet/src/test/resources/corpus/shorthand.dwl b/components/camel-datasonnet/src/test/resources/corpus/shorthand.dwl new file mode 100644 index 000000000000..b59196da160e --- /dev/null +++ b/components/camel-datasonnet/src/test/resources/corpus/shorthand.dwl @@ -0,0 +1,11 @@ +%dw 2.0 +output application/json +--- +{ + names: payload.items map upper($.name), + lines: payload.items map { line: $$, sku: $.@sku }, + byPriceDesc: (payload.items orderBy -$.price) map $.name, + discounted: (payload.items filter $.discount?) map $.name, + skus: (payload.items distinctBy $.@sku) map $.@sku, + totalQty: (payload.items map $.qty) reduce ($$ + $) +} diff --git a/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConverter.java b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConverter.java index 5545deaa13c9..b8f3984e488b 100644 --- a/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConverter.java +++ b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConverter.java @@ -16,6 +16,7 @@ */ package org.apache.camel.component.dataweave; +import java.lang.reflect.RecordComponent; import java.util.ArrayList; import java.util.List; @@ -32,6 +33,10 @@ public class DataWeaveConverter { private int todoCount; private int convertedCount; private boolean includeComments = true; + // The variables $ and $$ refer to while emitting the body of a shorthand lambda (payload map $.id); + // null outside a shorthand lambda + private String shorthandItem; + private String shorthandDoubleDollar; public DataWeaveConverter() { } @@ -153,12 +158,14 @@ public class DataWeaveConverter { return emitAttributeAccess(aa); } else if (node instanceof ExistenceCheck ec) { return emitExistenceCheck(ec); + } else if (node instanceof DoubleDollar && shorthandDoubleDollar != null) { + return shorthandDoubleDollar; } else if (node instanceof DoubleDollar) { - // $$ means accumulator only inside reduce. In map/filter/mapObject/pluck it means index or key. - // Outside the reduce-shorthand emitter this context is unknown, so emit a TODO. + // $$ means accumulator inside reduce and index inside map. In filter/mapObject/pluck it means index or key, + // which is not supported, so emit a TODO. todoCount++; return includeComments - ? "/* TODO: manual conversion needed -- $$ outside reduce context */" + "\nnull" + ? "/* TODO: manual conversion needed -- $$ outside map and reduce */" + "\nnull" : "null"; } else if (node instanceof ObjectLit obj) { return emitObjectLit(obj); @@ -180,6 +187,8 @@ public class DataWeaveConverter { return emitLambda(lam); } else if (node instanceof LambdaParam lp) { return lp.name(); + } else if (node instanceof LambdaShorthand ls && shorthandItem != null) { + return ls.fields().isEmpty() ? shorthandItem : shorthandItem + "." + String.join(".", ls.fields()); } else if (node instanceof LambdaShorthand ls) { return emitLambdaShorthand(ls); } else if (node instanceof MapExpr me) { @@ -304,6 +313,12 @@ public class DataWeaveConverter { if (ec.expr() instanceof AttributeAccess aa) { return "std.objectHas(" + emitNode(aa.object()) + ", \"@" + aa.attribute() + "\")"; } + if (ec.expr() instanceof LambdaShorthand ls && shorthandItem != null && !ls.fields().isEmpty()) { + // $.a.b? -> std.objectHas(x.a, "b") + List<String> fields = ls.fields(); + return "std.objectHas(" + emitNode(new LambdaShorthand(fields.subList(0, fields.size() - 1))) + ", \"" + + fields.get(fields.size() - 1) + "\")"; + } todoCount++; return includeComments ? "/* TODO: manual conversion needed -- existence check on non-field expression: " @@ -497,12 +512,11 @@ public class DataWeaveConverter { } return "std.map(function(" + paramNames.get(0) + ") " + body + ", " + collection + ")"; } - if (me.lambda() instanceof LambdaShorthand ls) { - // $.field -> function(x) x.field - String path = String.join(".", ls.fields()); - return "std.map(function(x) x." + path + ", " + collection + ")"; + if (references(me.lambda(), DoubleDollar.class)) { + // DW: map {i: $$, v: $} -- $$ is the index -- DS: std.mapWithIndex(function(i, x) body, collection) + return "std.mapWithIndex(function(i, x) " + emitShorthandScope(me.lambda(), "x", "i") + ", " + collection + ")"; } - return "std.map(" + emitNode(me.lambda()) + ", " + collection + ")"; + return "std.map(" + emitLambdaArg(me.lambda()) + ", " + collection + ")"; } private String emitFilter(FilterExpr fe) { @@ -512,7 +526,7 @@ public class DataWeaveConverter { String body = emitNode(lam.body()); return "std.filter(function(" + paramNames.get(0) + ") " + body + ", " + collection + ")"; } - return "std.filter(" + emitNode(fe.lambda()) + ", " + collection + ")"; + return "std.filter(" + emitLambdaArg(fe.lambda()) + ", " + collection + ")"; } private String emitReduce(ReduceExpr re) { @@ -537,106 +551,89 @@ public class DataWeaveConverter { // $$ is the accumulator ($$ -> acc) and $ is the current item ($ -> item). // DataWeave shorthand without an explicit initial value uses the first element // as the starting accumulator: std.foldl(function(acc, item) body, arr[1:], arr[0]). - String body = emitReduceShorthandBody(re.lambda()); + String body = emitShorthandScope(re.lambda(), "item", "acc"); return "(local _arr = " + collection + ";\n" + "if std.length(_arr) == 0 then null else std.foldl(function(acc, item) " + body + ", _arr[1:], _arr[0]))"; } /** - * Emit a reduce shorthand body, rewriting {@code $$} to {@code acc} and {@code $} (optionally with field access) to - * {@code item} or {@code item.field}. Falls back to normal {@code emitNode} for any sub-expression that doesn't - * contain shorthand references. + * Emit the function passed to a higher-order operator (map, filter, orderBy, ...) when it is not an explicit + * lambda. A body that uses {@code $} ({@code payload map {id: $.id}}) becomes {@code function(x) <body>} with + * {@code $} referring to {@code x}; anything else (such as a function reference) is emitted as-is. */ - private String emitReduceShorthandBody(DataWeaveAst node) { - if (node instanceof DoubleDollar) { - return "acc"; + private String emitLambdaArg(DataWeaveAst lambda) { + if (containsShorthand(lambda)) { + return "function(x) " + emitShorthandScope(lambda, "x", null); } - if (node instanceof LambdaShorthand ls) { - if (ls.fields().isEmpty()) { - return "item"; - } - return "item." + String.join(".", ls.fields()); - } - if (node instanceof BinaryOp op) { - String left = emitReduceShorthandBody(op.left()); - String right = emitReduceShorthandBody(op.right()); - return switch (op.op()) { - case "++" -> left + " + " + right; - case "and" -> left + " && " + right; - case "or" -> left + " || " + right; - default -> left + " " + op.op() + " " + right; - }; - } - if (node instanceof Parens p) { - return "(" + emitReduceShorthandBody(p.expr()) + ")"; - } - if (node instanceof FieldAccess fa) { - return emitReduceShorthandBody(fa.object()) + "." + fa.field(); - } - if (node instanceof IndexAccess ia) { - return emitReduceShorthandBody(ia.object()) + "[" + emitReduceShorthandBody(ia.index()) + "]"; - } - if (node instanceof UnaryOp op) { - return switch (op.op()) { - case "not" -> "!" + emitReduceShorthandBody(op.operand()); - default -> op.op() + emitReduceShorthandBody(op.operand()); - }; - } - // For anything else: if the sub-expression contains a shorthand reference ($ or $$) - // that emitNode cannot rewrite, emit a TODO to avoid silently producing wrong code. - // Pure literals and identifiers without shorthand references are safe to emit normally. - if (containsShorthand(node)) { - todoCount++; - return includeComments - ? "/* TODO: manual conversion needed -- reduce shorthand in unsupported context: " - + node.getClass().getSimpleName() + " */\nnull" - : "null"; + return emitNode(lambda); + } + + /** + * Emit the body of a shorthand lambda with {@code $} referring to {@code itemVar} and {@code $$} to + * {@code doubleDollarVar} (the reduce accumulator or the map index, or null when {@code $$} has no known meaning). + * A nested shorthand lambda opens its own scope, as {@code $} then refers to the inner item. + */ + private String emitShorthandScope(DataWeaveAst body, String itemVar, String doubleDollarVar) { + String oldItem = shorthandItem; + String oldDoubleDollar = shorthandDoubleDollar; + shorthandItem = itemVar; + shorthandDoubleDollar = doubleDollarVar; + try { + return emitNode(body); + } finally { + shorthandItem = oldItem; + shorthandDoubleDollar = oldDoubleDollar; } - return emitNode(node); } /** - * Returns true if the given AST node or any of its children contain a LambdaShorthand ($) or DoubleDollar ($$) that - * would be emitted incorrectly by the normal emitNode path in a reduce shorthand context. + * Returns true if the given AST node uses {@code $} or {@code $$} of the enclosing shorthand lambda. */ - private boolean containsShorthand(DataWeaveAst node) { - if (node == null) { + private static boolean containsShorthand(DataWeaveAst node) { + return references(node, LambdaShorthand.class) || references(node, DoubleDollar.class); + } + + /** + * Returns true if the given AST node uses {@code $} ({@link LambdaShorthand}) or {@code $$} ({@link DoubleDollar}) + * of the enclosing shorthand lambda. The function argument of a nested higher-order operator and the body of an + * explicit lambda are not searched, as {@code $} there does not refer to the enclosing item. + */ + private static boolean references(DataWeaveAst node, Class<? extends DataWeaveAst> type) { + if (node == null || node instanceof Lambda) { return false; } - if (node instanceof LambdaShorthand || node instanceof DoubleDollar) { + if (type.isInstance(node)) { return true; } - if (node instanceof BinaryOp op) { - return containsShorthand(op.left()) || containsShorthand(op.right()); - } - if (node instanceof UnaryOp op) { - return containsShorthand(op.operand()); - } - if (node instanceof Parens p) { - return containsShorthand(p.expr()); - } - if (node instanceof FieldAccess fa) { - return containsShorthand(fa.object()); - } - if (node instanceof FunctionCall fc) { - return fc.args().stream().anyMatch(this::containsShorthand); - } - if (node instanceof DefaultExpr def) { - return containsShorthand(def.expr()) || containsShorthand(def.fallback()); - } - if (node instanceof IfElse ie) { - return containsShorthand(ie.condition()) || containsShorthand(ie.thenExpr()) || containsShorthand(ie.elseExpr()); - } - if (node instanceof IndexAccess ia) { - return containsShorthand(ia.object()) || containsShorthand(ia.index()); - } - // Unknown node type: default to true (conservative — prevents silent wrong output). - // Only return false for nodes known to never contain shorthand (literals, Identifier). - if (node instanceof NumberLit || node instanceof StringLit || node instanceof BooleanLit - || node instanceof NullLit || node instanceof Identifier) { - return false; + for (RecordComponent component : node.getClass().getRecordComponents()) { + if (isHigherOrderLambda(node, component)) { + continue; + } + Object value; + try { + value = component.getAccessor().invoke(node); + } catch (ReflectiveOperationException e) { + throw new IllegalStateException(e); + } + if (value instanceof DataWeaveAst child && references(child, type)) { + return true; + } + if (value instanceof List<?> list) { + for (Object element : list) { + if (element instanceof DataWeaveAst child && references(child, type)) { + return true; + } + } + } } - return true; + return false; + } + + private static boolean isHigherOrderLambda(DataWeaveAst node, RecordComponent component) { + return "lambda".equals(component.getName()) + && (node instanceof MapExpr || node instanceof FilterExpr || node instanceof ReduceExpr + || node instanceof FlatMapExpr || node instanceof DistinctByExpr || node instanceof GroupByExpr + || node instanceof OrderByExpr); } private String emitFlatMap(FlatMapExpr fme) { @@ -646,7 +643,7 @@ public class DataWeaveConverter { String body = emitNode(lam.body()); return "std.flatMap(function(" + paramNames.get(0) + ") " + body + ", " + collection + ")"; } - return "std.flatMap(" + emitNode(fme.lambda()) + ", " + collection + ")"; + return "std.flatMap(" + emitLambdaArg(fme.lambda()) + ", " + collection + ")"; } private String emitDistinctBy(DistinctByExpr dbe) { @@ -658,6 +655,9 @@ public class DataWeaveConverter { // distinctBy keeps first occurrence per key -- use distinctBy helper return "c.distinctBy(" + collection + ", function(" + paramNames.get(0) + ") " + body + ")"; } + if (containsShorthand(dbe.lambda())) { + return "c.distinctBy(" + collection + ", " + emitLambdaArg(dbe.lambda()) + ")"; + } return "c.distinct(" + collection + ")"; } @@ -672,7 +672,7 @@ public class DataWeaveConverter { // If the key expression is already a string, std.toString() is a no-op. return "c.groupBy(" + collection + ", function(" + paramNames.get(0) + ") std.toString(" + body + "))"; } - return "c.groupBy(" + collection + ", function(x) std.toString((" + emitNode(gbe.lambda()) + ")(x)))"; + return "c.groupBy(" + collection + ", function(x) std.toString((" + emitLambdaArg(gbe.lambda()) + ")(x)))"; } private String emitOrderBy(OrderByExpr obe) { @@ -683,7 +683,7 @@ public class DataWeaveConverter { String body = emitNode(lam.body()); return "c.sortBy(" + collection + ", function(" + paramNames.get(0) + ") " + body + ")"; } - return "c.sortBy(" + collection + ", " + emitNode(obe.lambda()) + ")"; + return "c.sortBy(" + collection + ", " + emitLambdaArg(obe.lambda()) + ")"; } private String emitContains(ContainsExpr ce) { diff --git a/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java index 92508c3846b9..dfd12dd27358 100644 --- a/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java +++ b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java @@ -615,14 +615,51 @@ public class DataWeaveParser { private DataWeaveAst parseDollarShorthand() { advance(); // $ List<String> fields = new ArrayList<>(); - while (check(TokenType.DOT)) { + while (check(TokenType.DOT) && isTokenAhead(1, TokenType.IDENTIFIER)) { + advance(); // . + fields.add(current().value()); advance(); - if (check(TokenType.IDENTIFIER)) { - fields.add(current().value()); + } + DataWeaveAst expr = new DataWeaveAst.LambdaShorthand(fields); + // Selectors that belong to the shorthand itself ($.@id, $.*Item, $.a?, $[0], [email protected]). + // They are parsed here so they bind to $ also where the shorthand is a lambda argument + // (payload map $.@id), which is not followed by the general postfix operators. + while (true) { + if (check(TokenType.DOT) && isTokenAhead(1, TokenType.AT)) { + advance(); // . + advance(); // @ + String attr = current().value(); + advance(); + expr = new DataWeaveAst.AttributeAccess(expr, attr); + } else if (check(TokenType.DOT) && isTokenAhead(1, TokenType.STAR)) { + advance(); // . + advance(); // * + String field = current().value(); advance(); + expr = new DataWeaveAst.MultiValueSelector(expr, field); + } else if (check(TokenType.DOT) && isTokenAhead(1, TokenType.IDENTIFIER)) { + // a field after another selector, such as [email protected] + advance(); // . + String field = current().value(); + advance(); + expr = new DataWeaveAst.FieldAccess(expr, field); + } else if (check(TokenType.QUESTION)) { + advance(); // ? + expr = new DataWeaveAst.ExistenceCheck(expr); + } else if (check(TokenType.LBRACKET)) { + advance(); // [ + DataWeaveAst index = parseExpression(); + expect(TokenType.RBRACKET); + expr = new DataWeaveAst.IndexAccess(expr, index); + } else { + return expr; } } - return new DataWeaveAst.LambdaShorthand(fields); + } + + private boolean isTokenAhead(int offset, TokenType type) { + Token token = peekAhead(offset); + return token != null && token.type() == type; } private DataWeaveAst parsePrimary() { diff --git a/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveConverterTest.java b/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveConverterTest.java index 5ea0590561a5..95ea9018298c 100644 --- a/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveConverterTest.java +++ b/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveConverterTest.java @@ -686,7 +686,7 @@ class DataWeaveConverterTest { @Test void testReduceShorthandIndexAccess() { // DW: payload.items reduce ($$ + $[0]) -- shorthand body with index access on $ - // containsShorthand must recurse into IndexAccess, and emitReduceShorthandBody must handle it. + // containsShorthand must recurse into IndexAccess, and the shorthand scope must rewrite $ to item. // Expected body: acc + item[0] (not emitNode's function(x) x[0]) String result = converter.convertExpression("payload.items reduce ($$ + $[0])"); assertTrue(result.contains("std.foldl("), "Should use std.foldl, got: " + result); @@ -696,6 +696,121 @@ class DataWeaveConverterTest { assertEquals(0, converter.getTodoCount(), "Reduce shorthand index access should not produce TODOs, got: " + result); } + @Test + void testShorthandAttributeInMap() { + // DW: payload.items map $.@id -- the attribute selector belongs to $, not to the map result + String result = converter.convertExpression("payload.items map $.@id"); + assertEquals("std.map(function(x) x[\"@id\"], body.items)", result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testShorthandMultiValueInMap() { + // DW: payload.orders map $.*Item + String result = converter.convertExpression("payload.orders map $.*Item"); + assertEquals("std.map(function(x) std.map(function(x) x.Item, x), body.orders)", result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testShorthandExistenceInFilter() { + // DW: payload.items filter $.a.b? -- ? applies to $.a.b, not to the filter result + String result = converter.convertExpression("payload.items filter $.a.b?"); + assertEquals("std.filter(function(x) std.objectHas(x.a, \"b\"), body.items)", result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testShorthandIndexInMap() { + // DW: payload.rows map $[0] + String result = converter.convertExpression("payload.rows map $[0]"); + assertEquals("std.map(function(x) x[0], body.rows)", result); + } + + @Test + void testShorthandObjectInMap() { + // DW: payload.items map { id: $.id, name: $.name } -- the whole object is the lambda body + String result = converter.convertExpression("payload.items map { id: $.id, name: $.name }"); + assertTrue(result.startsWith("std.map(function(x) {"), "Object should be the lambda body, got: " + result); + assertTrue(result.contains("id: x.id"), "got: " + result); + assertTrue(result.contains("name: x.name"), "got: " + result); + assertFalse(result.contains("function(x) x.id"), "$ must not become a function per field, got: " + result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testShorthandFunctionCallInMap() { + // DW: payload.items map upper($.name) + String result = converter.convertExpression("payload.items map upper($.name)"); + assertEquals("std.map(function(x) std.asciiUpper(x.name), body.items)", result); + } + + @Test + void testShorthandIndexDoubleDollarInMap() { + // DW: payload.items map { line: $$, name: $.name } -- in map, $$ is the index + String result = converter.convertExpression("payload.items map { line: $$, name: $.name }"); + assertTrue(result.startsWith("std.mapWithIndex(function(i, x) {"), "got: " + result); + assertTrue(result.contains("line: i"), "got: " + result); + assertTrue(result.contains("name: x.name"), "got: " + result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testShorthandOrderByDescending() { + // DW: payload.items orderBy -$.price + String result = converter.convertExpression("payload.items orderBy -$.price"); + assertEquals("c.sortBy(body.items, function(x) -x.price)", result); + } + + @Test + void testShorthandDistinctBy() { + // DW: payload.items distinctBy $.id -- the key must not be dropped + String result = converter.convertExpression("payload.items distinctBy $.id"); + assertEquals("c.distinctBy(body.items, function(x) x.id)", result); + } + + @Test + void testShorthandNestedScopes() { + // DW: payload.orders map { id: $.id, skus: $.lines map $.sku } -- inner $ is the line, outer $ the order + String result = converter.convertExpression("payload.orders map { id: $.id, skus: $.lines map $.sku }"); + assertTrue(result.contains("id: x.id"), "got: " + result); + assertTrue(result.contains("skus: std.map(function(x) x.sku, x.lines)"), "got: " + result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testReduceShorthandAttributeAndMultiValue() { + // DW: payload.items reduce ($$ + $.@qty) and reduce ($$ ++ $.*Item) + String attr = converter.convertExpression("payload.items reduce ($$ + $.@qty)"); + assertTrue(attr.contains("function(acc, item) (acc + item[\"@qty\"])"), "got: " + attr); + String multi = converter.convertExpression("payload.items reduce ($$ ++ $.*Item)"); + assertTrue(multi.contains("function(acc, item) (acc + std.map(function(x) x.Item, item))"), "got: " + multi); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testReduceShorthandIfElseAndExistence() { + // DW: payload.items reduce (if ($.price?) $$ + $.price else $$) + String result = converter.convertExpression("payload.items reduce (if ($.price?) $$ + $.price else $$)"); + assertTrue(result.contains("if std.objectHas(item, \"price\") then acc + item.price else acc"), "got: " + result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testReduceShorthandDefaultAndFunctionCall() { + // DW: payload.items reduce ($$ + ($.qty default 0) * sizeOf($.tags)) + String result = converter.convertExpression("payload.items reduce ($$ + ($.qty default 0) * sizeOf($.tags))"); + assertTrue(result.contains("acc + (cml.defaultVal(item.qty, 0)) * std.length(item.tags)"), "got: " + result); + assertEquals(0, converter.getTodoCount()); + } + + @Test + void testDoubleDollarInFilterIsTodo() { + // DW: payload.items filter $$ > 0 -- $$ is the index in filter, which is not supported + converter.convertExpression("payload.items filter ($$ > 0)"); + assertEquals(1, converter.getTodoCount()); + } + @Test void testHeaderVarComplexValue() { // DW header var with a complex value (if/else) — parseExpression() handles if/else,
