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]