davsclaus commented on code in PR #27535:
URL: https://github.com/apache/camel/pull/27535#discussion_r4220146313


##########
components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConverter.java:
##########
@@ -479,7 +527,54 @@ private String emitReduce(ReduceExpr re) {
                        + collection + ", " + init + ")";
             }
         }
-        return "std.foldl(" + emitNode(re.lambda()) + ", " + collection + ", 
null)";
+        // Shorthand form: payload.items reduce ($$ + $.price)
+        // $$ 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());
+        return "local _arr = " + collection + ";\n"
+               + "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.
+     */
+    private String emitReduceShorthandBody(DataWeaveAst node) {
+        if (node instanceof DoubleDollar) {
+            return "acc";
+        }
+        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 UnaryOp op) {
+            return switch (op.op()) {
+                case "not" -> "!" + emitReduceShorthandBody(op.operand());
+                default -> op.op() + emitReduceShorthandBody(op.operand());
+            };
+        }
+        // For anything else (literals, identifiers, etc.) fall back to normal 
emission
+        return emitNode(node);

Review Comment:
   The shorthand rewrite reads well. One gap: anything not handled above falls 
back to `emitNode`, which still emits `$` as `function(x) x...`. For example 
`$$ + ($.price default 0)` becomes `acc + cml.defaultVal(function(x) x.price, 
0)`: wrong, and no TODO is counted. One option is a flag that 
`emitLambdaShorthand` and `DoubleDollar` check while emitting a reduce 
shorthand body, so every node type is covered. Another is to count a TODO when 
the fallback subtree contains a `LambdaShorthand`.



##########
components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveConverterTest.java:
##########
@@ -483,8 +484,174 @@ void testStringEscapesPreserved() {
         assertTrue(result.contains("\"\\n\""), "Newline escape should be 
preserved, got: " + result);
     }
 
+    // -- CAMEL-25324 fixes --
+
+    @Test
+    void testDoubleQuoteNoDoubleEscape() {
+        // DW: "say \"hi\"" -- the lexer stores the backslash-quote verbatim; 
do NOT double-escape on emit
+        String result = converter.convertExpression("\"say \\\"hi\\\"\"");
+        assertEquals("\"say \\\"hi\\\"\"", result);
+    }
+
+    @Test
+    void testStringInterpolation() {
+        // DW: "Hello $(payload.name)" -- Jsonnet has no string interpolation, 
emit as TODO
+        String result = converter.convertExpression("\"Hello 
$(payload.name)\"");
+        assertTrue(result.contains("TODO"), "String interpolation should be a 
TODO, got: " + result);
+        assertTrue(converter.getTodoCount() > 0);
+    }
+
+    @Test
+    void testAttributeAccess() {
+        // DW: payload.Order.@id -> DS: body.Order["@id"]
+        String result = converter.convertExpression("payload.Order.@id");
+        assertEquals("body.Order[\"@id\"]", result);
+    }
+
+    @Test
+    void testExistenceCheck() {
+        // DW: payload.a? -> DS: std.objectHas(body, "a") -- using 
std.objectHas for FieldAccess
+        String result = converter.convertExpression("payload.a?");
+        assertEquals("std.objectHas(body, \"a\")", result);
+        assertFalse(converter.needsCamelLib());
+    }
+
+    @Test
+    void testDoubleDollarInReduce() {
+        // DW: payload.items reduce ((item, acc = 0) -> acc + item.price) -- 
explicit lambda
+        String result = converter.convertExpression("payload.items reduce 
((item, acc = 0) -> acc + item.price)");
+        assertTrue(result.contains("std.foldl"), "Should use std.foldl, got: " 
+ result);
+        assertTrue(result.contains("function(acc, item)"), "acc and item 
should be swapped for foldl, got: " + result);
+    }
+
+    @Test
+    void testDoubleDollarLexedCorrectly() {
+        // $$ must lex as DOLLAR_DOLLAR and emit as 'acc' in the converter
+        String result = converter.convertExpression("$$");
+        assertEquals("acc", result);
+    }
+
+    @Test
+    void testVarDeclarationInHeader() {
+        // DW header var declarations must survive as local bindings in the 
body
+        String dw = """
+                %dw 2.0
+                output application/json
+                var rate = 0.08
+                ---
+                payload.price * rate
+                """;
+        String result = converter.convert(dw);
+        assertTrue(result.contains("local rate = 0.08"), "var rate must emit 
as local rate, got: " + result);
+        assertTrue(result.contains("body.price * rate"), "body expression must 
reference rate, got: " + result);
+    }
+
+    @Test
+    void testFunDeclarationInHeader() {
+        // DW header fun declarations must survive as local functions in the 
body
+        String dw = """
+                %dw 2.0
+                output application/json
+                fun double(x) = x * 2
+                ---
+                double(payload.value)
+                """;
+        String result = converter.convert(dw);
+        assertTrue(result.contains("local double(x) ="), "fun double must emit 
as local function, got: " + result);
+        assertTrue(result.contains("double(body.value)"), "body expression 
must call double, got: " + result);
+    }
+
+    @Test
+    void testTypedFunParams() {
+        // DW: fun f(a: Number): Number = a * 2  -- type annotations must be 
stripped
+        String result = converter.convertExpression("fun f(a: Number) = a * 
2\nf(payload.x)");
+        assertTrue(result.contains("local f(a) ="), "typed param should be 
stripped, got: " + result);
+        assertFalse(result.contains("Number"), "type annotation must not 
appear in output, got: " + result);
+    }
+
+    @Test
+    void testGroupByKeyStringified() {
+        // DW: payload.items groupBy ((i) -> i.qty) -- groupBy key must be 
stringified
+        String result = converter.convertExpression("payload.items groupBy 
((i) -> i.qty)");
+        assertTrue(result.contains("c.groupBy("), "Should use c.groupBy, got: 
" + result);
+        assertTrue(result.contains("std.toString("), "groupBy key must be 
stringified, got: " + result);
+    }
+
+    @Test
+    void testMultiValueSelectorXmlChildren() {
+        // DW: payload.Order.Items.*Item -> DS: std.map(function(x) x.Item, 
body.Order.Items)
+        String result = 
converter.convertExpression("payload.Order.Items.*Item");
+        assertEquals("std.map(function(x) x.Item, body.Order.Items)", result);
+        assertFalse(converter.needsCamelLib());
+    }
+
     // -- Helpers --
 
+    @Test
+    void testDoubleQuotedStringWithEscapedQuote() {
+        // DW: 'say "hi"' (single-quoted, stored by lexer as: say "hi")
+        // Output must be "say \"hi\"" -- the bare " must be escaped for valid 
Jsonnet
+        // Simulate: the lexer stores the string value without surrounding 
quotes
+        // We test through convertExpression using a DW-style literal to 
exercise emitStringLit
+        DataWeaveConverter c2 = new DataWeaveConverter();
+        // Build directly: StringLit with a value containing an unescaped 
double-quote
+        // The lexer for single-quoted strings would store: say "hi"
+        // We convert: "say \"hi\"" (double-quoted DW string with escaped 
quote) to verify no double-escape
+        String result = c2.convertExpression("\"say \\\"hi\\\"\"");

Review Comment:
   The rename makes the test honest, but its comments still describe a 
single-quoted DW string, and the single-quote path (which escapes the bare `"`) 
is still not tested. Since the lexer accepts `'`, something like 
`convertExpression("'say \"hi\"'")` expecting `"say \"hi\""` would cover it.



##########
components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java:
##########
@@ -719,6 +805,40 @@ private DataWeaveAst parseArrayLiteral() {
 
     // -- Token helpers --
 
+    /**
+     * Skip a DataWeave type expression after a colon. Handles: - Simple: 
{@code Number} - Generic:
+     * {@code Array<Number>}, {@code Array<String | Null>} - Union: {@code 
String | Null} - Object type: {@code {name:
+     * String}} Stops when it encounters a {@code ,}, {@code )}, {@code =} or 
EOF at depth 0.
+     */
+    private void skipTypeExpression() {
+        // Skip the leading type name (or opening brace/bracket)
+        if (!check(TokenType.EOF)) {

Review Comment:
   The Javadoc lists object types (`{name: String}`), but here only the `{` is 
consumed and the loop then stops, so `name : String }` leaks into the parameter 
list. Skipping a balanced `{...}` (as for `<...>`), or throwing so the 
construct is reported as unsupported, would close this.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to