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,

Reply via email to