This is an automated email from the ASF dual-hosted git repository.

gnodet pushed a commit to branch worktree-dataweave-to-datasonnet
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 2d79aeb13af474040ced7480d2153694af861536
Author: Guillaume Nodet <[email protected]>
AuthorDate: Fri Mar 20 23:09:09 2026 +0100

    Add DataWeave to DataSonnet transpiler in camel-jbang
    
    Add a `camel transform dataweave` CLI command that converts MuleSoft
    DataWeave 2.0 scripts into equivalent DataSonnet .ds files, enabling
    easier migration of Mule integrations to Apache Camel.
    
    The transpiler includes:
    - Hand-written recursive descent parser for DataWeave 2.0 syntax
    - AST-based conversion to DataSonnet with Camel CML functions
    - Support for field access, operators, type coercion, null handling,
      collection operations (map/filter/reduce/flatMap/groupBy/orderBy),
      string operations, if/else, var/fun declarations
    - Unsupported constructs flagged with TODO comments
    - 43 unit tests covering all conversion patterns
    
    CLI usage:
      camel transform dataweave --input flow.dwl --output flow.ds
      camel transform dataweave --input src/mule/ --output src/resources/
      camel transform dataweave -e 'payload.name default "unknown"'
    
    Depends on PR #22156 (camel-datasonnet CML enhancements).
    
    Co-Authored-By: Claude Opus 4.6 <[email protected]>
---
 .../dsl/jbang/core/commands/CamelJBangMain.java    |   1 +
 .../jbang/core/commands/TransformDataWeave.java    | 161 +++++
 .../core/commands/transform/DataWeaveAst.java      | 164 +++++
 .../commands/transform/DataWeaveConverter.java     | 618 +++++++++++++++++
 .../core/commands/transform/DataWeaveLexer.java    | 343 ++++++++++
 .../core/commands/transform/DataWeaveParser.java   | 729 +++++++++++++++++++++
 .../commands/transform/DataWeaveConverterTest.java | 393 +++++++++++
 .../test/resources/dataweave/collection-map.dwl    |  15 +
 .../src/test/resources/dataweave/event-message.dwl |  18 +
 .../src/test/resources/dataweave/null-handling.dwl |   9 +
 .../src/test/resources/dataweave/simple-rename.dwl |  11 +
 .../src/test/resources/dataweave/string-ops.dwl    |  11 +
 .../src/test/resources/dataweave/type-coercion.dwl |  10 +
 13 files changed, 2483 insertions(+)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
index 3627447d602c..bd03af9ee39c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/CamelJBangMain.java
@@ -197,6 +197,7 @@ public class CamelJBangMain implements Callable<Integer> {
                         .addSubcommand("source", new CommandLine(new 
CamelSourceTop(this))))
                 .addSubcommand("trace", new CommandLine(new 
CamelTraceAction(this)))
                 .addSubcommand("transform", new CommandLine(new 
TransformCommand(this))
+                        .addSubcommand("dataweave", new CommandLine(new 
TransformDataWeave(this)))
                         .addSubcommand("message", new CommandLine(new 
TransformMessageAction(this)))
                         .addSubcommand("route", new CommandLine(new 
TransformRoute(this))))
                 .addSubcommand("update", new CommandLine(new 
UpdateCommand(this))
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformDataWeave.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformDataWeave.java
new file mode 100644
index 000000000000..6fb61d12b57a
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/TransformDataWeave.java
@@ -0,0 +1,161 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands;
+
+import java.io.IOException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveConverter;
+import picocli.CommandLine;
+import picocli.CommandLine.Command;
+
+@Command(name = "dataweave",
+         description = "Convert DataWeave scripts to DataSonnet format",
+         sortOptions = false, showDefaultValues = true)
+public class TransformDataWeave extends CamelCommand {
+
+    @CommandLine.Option(names = { "--input", "-i" },
+                        description = "Input .dwl file or directory containing 
.dwl files")
+    private String input;
+
+    @CommandLine.Option(names = { "--output", "-o" },
+                        description = "Output .ds file or directory (defaults 
to stdout)")
+    private String output;
+
+    @CommandLine.Option(names = { "--expression", "-e" },
+                        description = "Inline DataWeave expression to convert")
+    private String expression;
+
+    @CommandLine.Option(names = { "--include-comments" }, defaultValue = 
"true",
+                        description = "Include conversion notes as comments in 
output")
+    private boolean includeComments = true;
+
+    public TransformDataWeave(CamelJBangMain main) {
+        super(main);
+    }
+
+    @Override
+    public Integer doCall() throws Exception {
+        if (expression != null) {
+            return convertExpression();
+        }
+        if (input != null) {
+            return convertFiles();
+        }
+
+        printer().println("Error: either --input or --expression must be 
specified");
+        return 1;
+    }
+
+    private int convertExpression() {
+        DataWeaveConverter converter = new DataWeaveConverter();
+        converter.setIncludeComments(includeComments);
+
+        String result;
+        if (expression.contains("%dw") || expression.contains("---")) {
+            result = converter.convert(expression);
+        } else {
+            result = converter.convertExpression(expression);
+        }
+
+        printer().println(result);
+        printSummary(converter, 1);
+        return converter.getTodoCount() > 0 ? 1 : 0;
+    }
+
+    private int convertFiles() throws IOException {
+        Path inputPath = Path.of(input);
+        if (!Files.exists(inputPath)) {
+            printer().println("Error: input path does not exist: " + input);
+            return 1;
+        }
+
+        List<Path> dwlFiles = new ArrayList<>();
+        if (Files.isDirectory(inputPath)) {
+            try (DirectoryStream<Path> stream = 
Files.newDirectoryStream(inputPath, "*.dwl")) {
+                for (Path entry : stream) {
+                    dwlFiles.add(entry);
+                }
+            }
+            if (dwlFiles.isEmpty()) {
+                printer().println("No .dwl files found in: " + input);
+                return 1;
+            }
+        } else {
+            dwlFiles.add(inputPath);
+        }
+
+        int totalTodos = 0;
+        int totalConverted = 0;
+
+        for (Path dwlFile : dwlFiles) {
+            DataWeaveConverter converter = new DataWeaveConverter();
+            converter.setIncludeComments(includeComments);
+
+            String dwContent = Files.readString(dwlFile);
+            String dsContent = converter.convert(dwContent);
+
+            totalTodos += converter.getTodoCount();
+            totalConverted += converter.getConvertedCount();
+
+            if (output != null) {
+                Path outputPath = resolveOutputPath(dwlFile, Path.of(output));
+                Files.createDirectories(outputPath.getParent());
+                Files.writeString(outputPath, dsContent);
+                printer().println("Converted: " + dwlFile + " -> " + 
outputPath);
+            } else {
+                if (dwlFiles.size() > 1) {
+                    printer().println("// === " + dwlFile.getFileName() + " 
===");
+                }
+                printer().println(dsContent);
+            }
+        }
+
+        printSummary(totalConverted, totalTodos, dwlFiles.size());
+        return totalTodos > 0 ? 1 : 0;
+    }
+
+    private Path resolveOutputPath(Path dwlFile, Path outputPath) {
+        String dsFileName = 
dwlFile.getFileName().toString().replaceFirst("\\.dwl$", ".ds");
+        if (Files.isDirectory(outputPath) || output.endsWith("/")) {
+            return outputPath.resolve(dsFileName);
+        }
+        // Single file output
+        return outputPath;
+    }
+
+    private void printSummary(DataWeaveConverter converter, int fileCount) {
+        printSummary(converter.getConvertedCount(), converter.getTodoCount(), 
fileCount);
+    }
+
+    private void printSummary(int converted, int todos, int fileCount) {
+        StringBuilder sb = new StringBuilder();
+        sb.append("\n");
+        if (fileCount > 1) {
+            sb.append("Files: ").append(fileCount).append(", ");
+        }
+        sb.append("Converted: ").append(converted).append(" expressions");
+        if (todos > 0) {
+            sb.append(", ").append(todos).append(" require manual review");
+        }
+        printer().printErr(sb.toString());
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveAst.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveAst.java
new file mode 100644
index 000000000000..56f71a98ba37
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveAst.java
@@ -0,0 +1,164 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.transform;
+
+import java.util.List;
+
+/**
+ * AST node types for DataWeave 2.0 expressions.
+ */
+public sealed interface DataWeaveAst {
+
+    record Script(Header header, DataWeaveAst body) implements DataWeaveAst {
+    }
+
+    record Header(String version, String outputType, List<InputDecl> inputs) 
implements DataWeaveAst {
+    }
+
+    record InputDecl(String name, String mediaType) implements DataWeaveAst {
+    }
+
+    // Literals
+    record StringLit(String value, boolean singleQuoted) implements 
DataWeaveAst {
+    }
+
+    record NumberLit(String value) implements DataWeaveAst {
+    }
+
+    record BooleanLit(boolean value) implements DataWeaveAst {
+    }
+
+    record NullLit() implements DataWeaveAst {
+    }
+
+    // Expressions
+    record Identifier(String name) implements DataWeaveAst {
+    }
+
+    record FieldAccess(DataWeaveAst object, String field) implements 
DataWeaveAst {
+    }
+
+    record IndexAccess(DataWeaveAst object, DataWeaveAst index) implements 
DataWeaveAst {
+    }
+
+    record MultiValueSelector(DataWeaveAst object, String field) implements 
DataWeaveAst {
+    }
+
+    record ObjectLit(List<ObjectEntry> entries) implements DataWeaveAst {
+    }
+
+    record ObjectEntry(DataWeaveAst key, DataWeaveAst value, boolean dynamic) 
implements DataWeaveAst {
+    }
+
+    record ArrayLit(List<DataWeaveAst> elements) implements DataWeaveAst {
+    }
+
+    record BinaryOp(String op, DataWeaveAst left, DataWeaveAst right) 
implements DataWeaveAst {
+    }
+
+    record UnaryOp(String op, DataWeaveAst operand) implements DataWeaveAst {
+    }
+
+    record IfElse(DataWeaveAst condition, DataWeaveAst thenExpr, DataWeaveAst 
elseExpr) implements DataWeaveAst {
+    }
+
+    record DefaultExpr(DataWeaveAst expr, DataWeaveAst fallback) implements 
DataWeaveAst {
+    }
+
+    record TypeCoercion(DataWeaveAst expr, String type, String format) 
implements DataWeaveAst {
+    }
+
+    record FunctionCall(String name, List<DataWeaveAst> args) implements 
DataWeaveAst {
+    }
+
+    record Lambda(List<LambdaParam> params, DataWeaveAst body) implements 
DataWeaveAst {
+    }
+
+    record LambdaParam(String name, DataWeaveAst defaultValue) implements 
DataWeaveAst {
+    }
+
+    record LambdaShorthand(List<String> fields) implements DataWeaveAst {
+    }
+
+    // Collection operations
+    record MapExpr(DataWeaveAst collection, DataWeaveAst lambda) implements 
DataWeaveAst {
+    }
+
+    record FilterExpr(DataWeaveAst collection, DataWeaveAst lambda) implements 
DataWeaveAst {
+    }
+
+    record ReduceExpr(DataWeaveAst collection, DataWeaveAst lambda) implements 
DataWeaveAst {
+    }
+
+    record FlatMapExpr(DataWeaveAst collection, DataWeaveAst lambda) 
implements DataWeaveAst {
+    }
+
+    record DistinctByExpr(DataWeaveAst collection, DataWeaveAst lambda) 
implements DataWeaveAst {
+    }
+
+    record GroupByExpr(DataWeaveAst collection, DataWeaveAst lambda) 
implements DataWeaveAst {
+    }
+
+    record OrderByExpr(DataWeaveAst collection, DataWeaveAst lambda) 
implements DataWeaveAst {
+    }
+
+    // String postfix operations
+    record ContainsExpr(DataWeaveAst string, DataWeaveAst substring) 
implements DataWeaveAst {
+    }
+
+    record StartsWithExpr(DataWeaveAst string, DataWeaveAst prefix) implements 
DataWeaveAst {
+    }
+
+    record EndsWithExpr(DataWeaveAst string, DataWeaveAst suffix) implements 
DataWeaveAst {
+    }
+
+    record SplitByExpr(DataWeaveAst string, DataWeaveAst separator) implements 
DataWeaveAst {
+    }
+
+    record JoinByExpr(DataWeaveAst array, DataWeaveAst separator) implements 
DataWeaveAst {
+    }
+
+    record ReplaceExpr(DataWeaveAst string, DataWeaveAst target, DataWeaveAst 
replacement) implements DataWeaveAst {
+    }
+
+    // Variable and function declarations
+    record VarDecl(String name, DataWeaveAst value, DataWeaveAst body) 
implements DataWeaveAst {
+    }
+
+    record FunDecl(String name, List<String> params, DataWeaveAst funBody, 
DataWeaveAst next) implements DataWeaveAst {
+    }
+
+    // Match expression
+    record MatchExpr(DataWeaveAst expr, String originalText) implements 
DataWeaveAst {
+    }
+
+    // Type check
+    record TypeCheck(DataWeaveAst expr, String type) implements DataWeaveAst {
+    }
+
+    // Unsupported construct (kept as raw text)
+    record Unsupported(String originalText, String reason) implements 
DataWeaveAst {
+    }
+
+    // Parenthesized expression (for preserving grouping)
+    record Parens(DataWeaveAst expr) implements DataWeaveAst {
+    }
+
+    // Block of local declarations followed by an expression
+    record Block(List<DataWeaveAst> declarations, DataWeaveAst expr) 
implements DataWeaveAst {
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveConverter.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveConverter.java
new file mode 100644
index 000000000000..d4acb1dcff99
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveConverter.java
@@ -0,0 +1,618 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.transform;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveAst.*;
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.Token;
+
+/**
+ * Converts DataWeave 2.0 scripts to DataSonnet. Parses the DataWeave input, 
walks the AST, and emits equivalent
+ * DataSonnet code.
+ */
+public class DataWeaveConverter {
+
+    private boolean needsCamelLib;
+    private int todoCount;
+    private int convertedCount;
+    private boolean includeComments = true;
+
+    public DataWeaveConverter() {
+    }
+
+    public void setIncludeComments(boolean includeComments) {
+        this.includeComments = includeComments;
+    }
+
+    public int getTodoCount() {
+        return todoCount;
+    }
+
+    public int getConvertedCount() {
+        return convertedCount;
+    }
+
+    public boolean needsCamelLib() {
+        return needsCamelLib;
+    }
+
+    /**
+     * Convert a full DataWeave script (with header) to DataSonnet.
+     */
+    public String convert(String dataWeave) {
+        needsCamelLib = false;
+        todoCount = 0;
+        convertedCount = 0;
+
+        DataWeaveLexer lexer = new DataWeaveLexer(dataWeave);
+        List<Token> tokens = lexer.tokenize();
+        DataWeaveParser parser = new DataWeaveParser(tokens);
+        DataWeaveAst ast = parser.parse();
+
+        return emit(ast);
+    }
+
+    /**
+     * Convert a single DataWeave expression (no header) to DataSonnet.
+     */
+    public String convertExpression(String expression) {
+        needsCamelLib = false;
+        todoCount = 0;
+        convertedCount = 0;
+
+        DataWeaveLexer lexer = new DataWeaveLexer(expression);
+        List<Token> tokens = lexer.tokenize();
+        DataWeaveParser parser = new DataWeaveParser(tokens);
+        DataWeaveAst ast = parser.parseExpressionOnly();
+
+        return emitNode(ast);
+    }
+
+    // ── Emission ──
+
+    private String emit(DataWeaveAst node) {
+        if (node instanceof Script script) {
+            return emitScript(script);
+        }
+        return emitNode(node);
+    }
+
+    private String emitScript(Script script) {
+        StringBuilder sb = new StringBuilder();
+
+        // Emit DataSonnet header
+        Header header = script.header();
+        if (header.outputType() != null) {
+            sb.append("/** DataSonnet\n");
+            sb.append("version=").append(header.version()).append("\n");
+            sb.append("output ").append(header.outputType()).append("\n");
+            for (InputDecl input : header.inputs()) {
+                sb.append("input ").append(input.name()).append(" 
").append(input.mediaType()).append("\n");
+            }
+            sb.append("*/\n");
+        }
+
+        // First pass: emit body to determine if camel lib is needed
+        String body = emitNode(script.body());
+
+        // Add camel lib import if needed
+        if (needsCamelLib) {
+            sb.append("local c = import 'camel.libsonnet';\n");
+        }
+
+        sb.append(body);
+        return sb.toString();
+    }
+
+    private String emitNode(DataWeaveAst node) {
+        if (node == null) {
+            return "";
+        }
+
+        convertedCount++;
+
+        if (node instanceof Script s) {
+            return emitScript(s);
+        } else if (node instanceof Header) {
+            return "";
+        } else if (node instanceof InputDecl) {
+            return "";
+        } else if (node instanceof StringLit s) {
+            return emitStringLit(s);
+        } else if (node instanceof NumberLit n) {
+            return n.value();
+        } else if (node instanceof BooleanLit b) {
+            return String.valueOf(b.value());
+        } else if (node instanceof NullLit) {
+            return "null";
+        } else if (node instanceof Identifier id) {
+            return emitIdentifier(id);
+        } else if (node instanceof FieldAccess fa) {
+            return emitFieldAccess(fa);
+        } else if (node instanceof IndexAccess ia) {
+            return emitNode(ia.object()) + "[" + emitNode(ia.index()) + "]";
+        } else if (node instanceof MultiValueSelector mv) {
+            return emitMultiValueSelector(mv);
+        } else if (node instanceof ObjectLit obj) {
+            return emitObjectLit(obj);
+        } else if (node instanceof ArrayLit arr) {
+            return emitArrayLit(arr);
+        } else if (node instanceof BinaryOp op) {
+            return emitBinaryOp(op);
+        } else if (node instanceof UnaryOp op) {
+            return emitUnaryOp(op);
+        } else if (node instanceof IfElse ie) {
+            return emitIfElse(ie);
+        } else if (node instanceof DefaultExpr def) {
+            return emitDefault(def);
+        } else if (node instanceof TypeCoercion tc) {
+            return emitTypeCoercion(tc);
+        } else if (node instanceof FunctionCall fc) {
+            return emitFunctionCall(fc);
+        } else if (node instanceof Lambda lam) {
+            return emitLambda(lam);
+        } else if (node instanceof LambdaParam lp) {
+            return lp.name();
+        } else if (node instanceof LambdaShorthand ls) {
+            return emitLambdaShorthand(ls);
+        } else if (node instanceof MapExpr me) {
+            return emitMap(me);
+        } else if (node instanceof FilterExpr fe) {
+            return emitFilter(fe);
+        } else if (node instanceof ReduceExpr re) {
+            return emitReduce(re);
+        } else if (node instanceof FlatMapExpr fme) {
+            return emitFlatMap(fme);
+        } else if (node instanceof DistinctByExpr dbe) {
+            return emitDistinctBy(dbe);
+        } else if (node instanceof GroupByExpr gbe) {
+            return emitGroupBy(gbe);
+        } else if (node instanceof OrderByExpr obe) {
+            return emitOrderBy(obe);
+        } else if (node instanceof ContainsExpr ce) {
+            return emitContains(ce);
+        } else if (node instanceof StartsWithExpr swe) {
+            return emitStartsWith(swe);
+        } else if (node instanceof EndsWithExpr ewe) {
+            return emitEndsWith(ewe);
+        } else if (node instanceof SplitByExpr sbe) {
+            return emitSplitBy(sbe);
+        } else if (node instanceof JoinByExpr jbe) {
+            return emitJoinBy(jbe);
+        } else if (node instanceof ReplaceExpr re) {
+            return emitReplace(re);
+        } else if (node instanceof VarDecl vd) {
+            return emitVarDecl(vd);
+        } else if (node instanceof FunDecl fd) {
+            return emitFunDecl(fd);
+        } else if (node instanceof MatchExpr me) {
+            return emitMatch(me);
+        } else if (node instanceof TypeCheck tc) {
+            return emitTypeCheck(tc);
+        } else if (node instanceof Unsupported u) {
+            return emitUnsupported(u);
+        } else if (node instanceof Parens p) {
+            return "(" + emitNode(p.expr()) + ")";
+        } else if (node instanceof Block b) {
+            return emitBlock(b);
+        }
+        return "";
+    }
+
+    private String emitStringLit(StringLit s) {
+        // DataSonnet uses double quotes for strings, single quotes for 
identifiers
+        String escaped = s.value().replace("\\", "\\\\");
+        // If the string contains single quotes from DW, keep them
+        return "\"" + escaped.replace("\"", "\\\"") + "\"";
+    }
+
+    private String emitIdentifier(Identifier id) {
+        return switch (id.name()) {
+            case "payload" -> "body";
+            case "flowVars" -> "cml.variable"; // DW 1.0
+            default -> id.name();
+        };
+    }
+
+    private String emitFieldAccess(FieldAccess fa) {
+        // Special handling for payload.x -> body.x
+        // vars.x -> cml.variable('x')
+        // attributes.headers.x -> cml.header('x')
+        // attributes.queryParams.x -> cml.header('x')
+
+        if (fa.object() instanceof Identifier id) {
+            if ("vars".equals(id.name())) {
+                return "cml.variable('" + fa.field() + "')";
+            }
+            if ("flowVars".equals(id.name())) {
+                return "cml.variable('" + fa.field() + "')";
+            }
+        }
+
+        if (fa.object() instanceof FieldAccess outer) {
+            if (outer.object() instanceof Identifier id && 
"attributes".equals(id.name())) {
+                if ("headers".equals(outer.field()) || 
"queryParams".equals(outer.field())) {
+                    return "cml.header('" + fa.field() + "')";
+                }
+            }
+        }
+
+        return emitNode(fa.object()) + "." + fa.field();
+    }
+
+    private String emitMultiValueSelector(MultiValueSelector mv) {
+        todoCount++;
+        String comment = includeComments
+                ? " // TODO: manual conversion needed — multi-value selector 
.*" + mv.field()
+                : "";
+        return emitNode(mv.object()) + "." + mv.field() + comment;
+    }
+
+    private String emitObjectLit(ObjectLit obj) {
+        if (obj.entries().isEmpty()) {
+            return "{}";
+        }
+        StringBuilder sb = new StringBuilder("{\n");
+        for (int i = 0; i < obj.entries().size(); i++) {
+            ObjectEntry entry = obj.entries().get(i);
+            String key;
+            if (entry.dynamic()) {
+                key = "[" + emitNode(entry.key()) + "]";
+            } else if (entry.key() instanceof Identifier id) {
+                key = id.name();
+            } else if (entry.key() instanceof StringLit sl) {
+                key = "\"" + sl.value() + "\"";
+            } else {
+                key = emitNode(entry.key());
+            }
+            sb.append("    ").append(key).append(": 
").append(emitNode(entry.value()));
+            if (i < obj.entries().size() - 1) {
+                sb.append(",");
+            }
+            sb.append("\n");
+        }
+        sb.append("}");
+        return sb.toString();
+    }
+
+    private String emitArrayLit(ArrayLit arr) {
+        if (arr.elements().isEmpty()) {
+            return "[]";
+        }
+        List<String> parts = new ArrayList<>();
+        for (DataWeaveAst element : arr.elements()) {
+            parts.add(emitNode(element));
+        }
+        return "[" + String.join(", ", parts) + "]";
+    }
+
+    private String emitBinaryOp(BinaryOp op) {
+        String left = emitNode(op.left());
+        String right = emitNode(op.right());
+        return switch (op.op()) {
+            case "++" -> left + " + " + right; // DW concat -> DS concat
+            case "and" -> left + " && " + right;
+            case "or" -> left + " || " + right;
+            default -> left + " " + op.op() + " " + right;
+        };
+    }
+
+    private String emitUnaryOp(UnaryOp op) {
+        return switch (op.op()) {
+            case "not" -> "!" + emitNode(op.operand());
+            default -> op.op() + emitNode(op.operand());
+        };
+    }
+
+    private String emitIfElse(IfElse ie) {
+        String cond = emitNode(ie.condition());
+        String thenPart = emitNode(ie.thenExpr());
+        if (ie.elseExpr() != null) {
+            String elsePart = emitNode(ie.elseExpr());
+            return "if " + cond + " then " + thenPart + " else " + elsePart;
+        }
+        return "if " + cond + " then " + thenPart;
+    }
+
+    private String emitDefault(DefaultExpr def) {
+        String expr = emitNode(def.expr());
+        String fallback = emitNode(def.fallback());
+        return "cml.defaultVal(" + expr + ", " + fallback + ")";
+    }
+
+    private String emitTypeCoercion(TypeCoercion tc) {
+        if (tc.format() != null) {
+            // Optimize: now() as String {format: "..."} -> cml.now("...")
+            if ("String".equals(tc.type()) && tc.expr() instanceof 
FunctionCall fc && "now".equals(fc.name())) {
+                return "cml.nowFmt(\"" + tc.format() + "\")";
+            }
+            String expr = emitNode(tc.expr());
+            // as String {format: "..."} -> cml.formatDate(expr, "...")
+            if ("String".equals(tc.type())) {
+                return "cml.formatDate(" + expr + ", \"" + tc.format() + "\")";
+            }
+            // as Date {format: "..."} -> cml.parseDate(expr, "...")
+            if ("Date".equals(tc.type()) || "DateTime".equals(tc.type()) || 
"LocalDateTime".equals(tc.type())) {
+                return "cml.parseDate(" + expr + ", \"" + tc.format() + "\")";
+            }
+        }
+        String expr = emitNode(tc.expr());
+        return switch (tc.type()) {
+            case "Number" -> "cml.toInteger(" + expr + ")";
+            case "String" -> "std.toString(" + expr + ")";
+            case "Boolean" -> "cml.toBoolean(" + expr + ")";
+            default -> {
+                todoCount++;
+                yield expr + (includeComments ? " // TODO: manual conversion 
needed — as " + tc.type() : "");
+            }
+        };
+    }
+
+    private String emitFunctionCall(FunctionCall fc) {
+        List<String> args = new ArrayList<>();
+        for (DataWeaveAst arg : fc.args()) {
+            args.add(emitNode(arg));
+        }
+        String argStr = String.join(", ", args);
+
+        return switch (fc.name()) {
+            case "sizeOf" -> "std.length(" + argStr + ")";
+            case "upper" -> "std.asciiUpper(" + argStr + ")";
+            case "lower" -> "std.asciiLower(" + argStr + ")";
+            case "trim" -> {
+                needsCamelLib = true;
+                yield "c.trim(" + argStr + ")";
+            }
+            case "capitalize" -> {
+                needsCamelLib = true;
+                yield "c.capitalize(" + argStr + ")";
+            }
+            case "now" -> args.isEmpty() ? "cml.now()" : "cml.now(" + argStr + 
")";
+            case "uuid" -> "cml.uuid()";
+            case "p" -> "cml.properties(" + argStr + ")";
+            case "typeOf" -> "cml.typeOf(" + argStr + ")";
+            case "isEmpty" -> "cml.isEmpty(" + argStr + ")";
+            case "isBlank" -> "cml.isEmpty(" + argStr + ")";
+            case "abs" -> "std.abs(" + argStr + ")";
+            case "ceil" -> "std.ceil(" + argStr + ")";
+            case "floor" -> "std.floor(" + argStr + ")";
+            case "round" -> "std.round(" + argStr + ")";
+            case "sqrt" -> "std.sqrt(" + argStr + ")";
+            case "sum" -> {
+                needsCamelLib = true;
+                yield "c.sum(" + argStr + ")";
+            }
+            case "min" -> "std.min(" + argStr + ")";
+            case "max" -> "std.max(" + argStr + ")";
+            case "read" -> {
+                todoCount++;
+                yield "std.parseJson(" + argStr + ")"
+                      + (includeComments ? " // TODO: verify — DW read() may 
handle multiple formats" : "");
+            }
+            case "write" -> {
+                todoCount++;
+                yield "std.manifestJsonEx(" + argStr + ", \"  \")"
+                      + (includeComments ? " // TODO: verify — DW write() may 
handle multiple formats" : "");
+            }
+            default -> fc.name() + "(" + argStr + ")";
+        };
+    }
+
+    private String emitLambda(Lambda lam) {
+        List<String> paramNames = new ArrayList<>();
+        for (LambdaParam p : lam.params()) {
+            paramNames.add(p.name());
+        }
+        return "function(" + String.join(", ") + ") " + emitNode(lam.body());
+    }
+
+    private String emitLambdaShorthand(LambdaShorthand ls) {
+        StringBuilder sb = new StringBuilder("$");
+        for (String field : ls.fields()) {
+            sb.append(".").append(field);
+        }
+        return sb.toString();
+    }
+
+    private String emitMap(MapExpr me) {
+        String collection = emitNode(me.collection());
+        if (me.lambda() instanceof Lambda lam) {
+            List<String> paramNames = lambdaParamNames(lam);
+            String body = emitNode(lam.body());
+            if (paramNames.size() == 2) {
+                // map with index: std.mapWithIndex(function(item, idx) body, 
collection)
+                return "std.mapWithIndex(function(" + paramNames.get(0) + ", " 
+ paramNames.get(1)
+                       + ") " + body + ", " + collection + ")";
+            }
+            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 + ")";
+        }
+        return "std.map(" + emitNode(me.lambda()) + ", " + collection + ")";
+    }
+
+    private String emitFilter(FilterExpr fe) {
+        String collection = emitNode(fe.collection());
+        if (fe.lambda() instanceof Lambda lam) {
+            List<String> paramNames = lambdaParamNames(lam);
+            String body = emitNode(lam.body());
+            return "std.filter(function(" + paramNames.get(0) + ") " + body + 
", " + collection + ")";
+        }
+        return "std.filter(" + emitNode(fe.lambda()) + ", " + collection + ")";
+    }
+
+    private String emitReduce(ReduceExpr re) {
+        String collection = emitNode(re.collection());
+        if (re.lambda() instanceof Lambda lam) {
+            // DataWeave reduce: (item, acc = init) -> expr
+            // DataSonnet foldl: function(acc, item) expr, arr, init
+            // NOTE: parameter order is SWAPPED
+            List<LambdaParam> params = lam.params();
+            if (params.size() >= 2) {
+                String itemParam = params.get(0).name();
+                String accParam = params.get(1).name();
+                DataWeaveAst initValue = params.get(1).defaultValue();
+                String init = initValue != null ? emitNode(initValue) : "null";
+                String body = emitNode(lam.body());
+                // Swap acc and item in the function signature for std.foldl
+                return "std.foldl(function(" + accParam + ", " + itemParam + 
") " + body + ", "
+                       + collection + ", " + init + ")";
+            }
+        }
+        todoCount++;
+        return "std.foldl(" + emitNode(re.lambda()) + ", " + collection + ", 
null)"
+               + (includeComments ? " // TODO: verify reduce conversion" : "");
+    }
+
+    private String emitFlatMap(FlatMapExpr fme) {
+        String collection = emitNode(fme.collection());
+        if (fme.lambda() instanceof Lambda lam) {
+            List<String> paramNames = lambdaParamNames(lam);
+            String body = emitNode(lam.body());
+            return "std.flatMap(function(" + paramNames.get(0) + ") " + body + 
", " + collection + ")";
+        }
+        return "std.flatMap(" + emitNode(fme.lambda()) + ", " + collection + 
")";
+    }
+
+    private String emitDistinctBy(DistinctByExpr dbe) {
+        needsCamelLib = true;
+        String collection = emitNode(dbe.collection());
+        if (dbe.lambda() instanceof Lambda lam) {
+            List<String> paramNames = lambdaParamNames(lam);
+            String body = emitNode(lam.body());
+            return "c.distinct(std.map(function(" + paramNames.get(0) + ") " + 
body + ", " + collection + "))";
+        }
+        return "c.distinct(" + collection + ")";
+    }
+
+    private String emitGroupBy(GroupByExpr gbe) {
+        needsCamelLib = true;
+        String collection = emitNode(gbe.collection());
+        if (gbe.lambda() instanceof Lambda lam) {
+            List<String> paramNames = lambdaParamNames(lam);
+            String body = emitNode(lam.body());
+            return "c.groupBy(" + collection + ", function(" + 
paramNames.get(0) + ") " + body + ")";
+        }
+        return "c.groupBy(" + collection + ", " + emitNode(gbe.lambda()) + ")";
+    }
+
+    private String emitOrderBy(OrderByExpr obe) {
+        String collection = emitNode(obe.collection());
+        if (obe.lambda() instanceof Lambda lam) {
+            List<String> paramNames = lambdaParamNames(lam);
+            String body = emitNode(lam.body());
+            return "std.sort(" + collection + ", function(" + 
paramNames.get(0) + ") " + body + ")";
+        }
+        todoCount++;
+        return "std.sort(" + collection + ")"
+               + (includeComments ? " // TODO: verify orderBy conversion" : 
"");
+    }
+
+    private String emitContains(ContainsExpr ce) {
+        needsCamelLib = true;
+        return "c.contains(" + emitNode(ce.string()) + ", " + 
emitNode(ce.substring()) + ")";
+    }
+
+    private String emitStartsWith(StartsWithExpr swe) {
+        return "std.startsWith(" + emitNode(swe.string()) + ", " + 
emitNode(swe.prefix()) + ")";
+    }
+
+    private String emitEndsWith(EndsWithExpr ewe) {
+        return "std.endsWith(" + emitNode(ewe.string()) + ", " + 
emitNode(ewe.suffix()) + ")";
+    }
+
+    private String emitSplitBy(SplitByExpr sbe) {
+        return "std.split(" + emitNode(sbe.string()) + ", " + 
emitNode(sbe.separator()) + ")";
+    }
+
+    private String emitJoinBy(JoinByExpr jbe) {
+        return "std.join(" + emitNode(jbe.separator()) + ", " + 
emitNode(jbe.array()) + ")";
+    }
+
+    private String emitReplace(ReplaceExpr re) {
+        return "std.strReplace(" + emitNode(re.string()) + ", " + 
emitNode(re.target()) + ", "
+               + emitNode(re.replacement()) + ")";
+    }
+
+    private String emitVarDecl(VarDecl vd) {
+        String value = emitNode(vd.value());
+        String body = vd.body() != null ? emitNode(vd.body()) : "";
+        return "local " + vd.name() + " = " + value + ";\n" + body;
+    }
+
+    private String emitFunDecl(FunDecl fd) {
+        String params = String.join(", ", fd.params());
+        String funBody = emitNode(fd.funBody());
+        String next = fd.next() != null ? emitNode(fd.next()) : "";
+        return "local " + fd.name() + "(" + params + ") = " + funBody + ";\n" 
+ next;
+    }
+
+    private String emitBlock(Block block) {
+        StringBuilder sb = new StringBuilder();
+        for (DataWeaveAst decl : block.declarations()) {
+            sb.append(emitNode(decl));
+        }
+        sb.append(emitNode(block.expr()));
+        return sb.toString();
+    }
+
+    private String emitMatch(MatchExpr me) {
+        todoCount++;
+        return (includeComments
+                ? "// TODO: manual conversion needed — match expression\n// " 
+ me.originalText() + "\n"
+                : "")
+               + "null";
+    }
+
+    private String emitTypeCheck(TypeCheck tc) {
+        String expr = emitNode(tc.expr());
+        return switch (tc.type()) {
+            case "String" -> "std.isString(" + expr + ")";
+            case "Number" -> "std.isNumber(" + expr + ")";
+            case "Boolean" -> "std.isBoolean(" + expr + ")";
+            case "Object" -> "std.isObject(" + expr + ")";
+            case "Array" -> "std.isArray(" + expr + ")";
+            case "Null" -> expr + " == null";
+            default -> {
+                todoCount++;
+                yield "cml.typeOf(" + expr + ") == \"" + 
tc.type().toLowerCase() + "\""
+                      + (includeComments ? " // TODO: verify type check" : "");
+            }
+        };
+    }
+
+    private String emitUnsupported(Unsupported u) {
+        todoCount++;
+        convertedCount--;
+        return includeComments
+                ? "// TODO: manual conversion needed — " + u.reason() + ": " + 
u.originalText() + "\nnull"
+                : "null";
+    }
+
+    private List<String> lambdaParamNames(Lambda lam) {
+        List<String> names = new ArrayList<>();
+        for (LambdaParam p : lam.params()) {
+            names.add(p.name());
+        }
+        return names;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexer.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexer.java
new file mode 100644
index 000000000000..42095d9fd643
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveLexer.java
@@ -0,0 +1,343 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.transform;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Tokenizer for DataWeave 2.0 scripts.
+ */
+public class DataWeaveLexer {
+
+    public enum TokenType {
+        // Literals
+        STRING,
+        NUMBER,
+        BOOLEAN,
+        NULL_LIT,
+        // Identifiers and keywords
+        IDENTIFIER,
+        // Operators
+        PLUS,
+        MINUS,
+        STAR,
+        SLASH,
+        PLUSPLUS,
+        EQ,
+        NEQ,
+        GT,
+        GE,
+        LT,
+        LE,
+        AND,
+        OR,
+        NOT,
+        // Punctuation
+        DOT,
+        COMMA,
+        COLON,
+        ARROW,
+        SEMICOLON,
+        LPAREN,
+        RPAREN,
+        LBRACE,
+        RBRACE,
+        LBRACKET,
+        RBRACKET,
+        DOLLAR,
+        // Special
+        HEADER_SEPARATOR, // ---
+        PERCENT,          // %
+        TILDE,            // ~
+        EOF
+    }
+
+    public record Token(TokenType type, String value, int line, int col) {
+        @Override
+        public String toString() {
+            return type + "(" + value + ")@" + line + ":" + col;
+        }
+    }
+
+    private static final Set<String> KEYWORDS = Set.of(
+            "map", "filter", "reduce", "flatMap", "distinctBy", "groupBy", 
"orderBy",
+            "if", "else", "and", "or", "not", "default", "as", "is",
+            "contains", "startsWith", "endsWith", "splitBy", "joinBy", 
"replace", "with",
+            "sizeOf", "upper", "lower", "trim", "capitalize", "now", "uuid", 
"p",
+            "payload", "vars", "attributes", "flowVars",
+            "var", "fun", "match", "do", "using",
+            "true", "false", "null",
+            "output", "input", "import");
+
+    private final String input;
+    private int pos;
+    private int line;
+    private int col;
+
+    public DataWeaveLexer(String input) {
+        this.input = input;
+        this.pos = 0;
+        this.line = 1;
+        this.col = 1;
+    }
+
+    public List<Token> tokenize() {
+        List<Token> tokens = new ArrayList<>();
+        while (pos < input.length()) {
+            skipWhitespaceAndComments();
+            if (pos >= input.length()) {
+                break;
+            }
+
+            Token token = readToken();
+            if (token != null) {
+                tokens.add(token);
+            }
+        }
+        tokens.add(new Token(TokenType.EOF, "", line, col));
+        return tokens;
+    }
+
+    private void skipWhitespaceAndComments() {
+        while (pos < input.length()) {
+            char c = input.charAt(pos);
+            if (c == ' ' || c == '\t' || c == '\r') {
+                advance();
+            } else if (c == '\n') {
+                advance();
+            } else if (c == '/' && pos + 1 < input.length() && 
input.charAt(pos + 1) == '/') {
+                // Line comment
+                while (pos < input.length() && input.charAt(pos) != '\n') {
+                    advance();
+                }
+            } else if (c == '/' && pos + 1 < input.length() && 
input.charAt(pos + 1) == '*') {
+                // Block comment
+                advance(); // /
+                advance(); // *
+                while (pos + 1 < input.length()
+                        && !(input.charAt(pos) == '*' && input.charAt(pos + 1) 
== '/')) {
+                    advance();
+                }
+                if (pos + 1 < input.length()) {
+                    advance(); // *
+                    advance(); // /
+                }
+            } else {
+                break;
+            }
+        }
+    }
+
+    private Token readToken() {
+        int startLine = line;
+        int startCol = col;
+        char c = input.charAt(pos);
+
+        // Header separator ---
+        if (c == '-' && pos + 2 < input.length()
+                && input.charAt(pos + 1) == '-' && input.charAt(pos + 2) == 
'-') {
+            // Make sure it's not a negative number context
+            if (pos == 0 || isHeaderSeparatorContext()) {
+                advance();
+                advance();
+                advance();
+                return new Token(TokenType.HEADER_SEPARATOR, "---", startLine, 
startCol);
+            }
+        }
+
+        // Strings
+        if (c == '"' || c == '\'') {
+            return readString(c, startLine, startCol);
+        }
+
+        // Numbers
+        if (Character.isDigit(c) || (c == '-' && pos + 1 < input.length() && 
Character.isDigit(input.charAt(pos + 1))
+                && !isPreviousTokenValueLike())) {
+            return readNumber(startLine, startCol);
+        }
+
+        // Identifiers and keywords
+        if (Character.isLetter(c) || c == '_') {
+            return readIdentifier(startLine, startCol);
+        }
+
+        // Operators and punctuation
+        return readOperator(startLine, startCol);
+    }
+
+    private boolean isHeaderSeparatorContext() {
+        // Look backwards to see if we're at the start of a line (after 
whitespace)
+        int i = pos - 1;
+        while (i >= 0 && (input.charAt(i) == ' ' || input.charAt(i) == '\t')) {
+            i--;
+        }
+        return i < 0 || input.charAt(i) == '\n';
+    }
+
+    private boolean isPreviousTokenValueLike() {
+        // Look back to see if the previous non-whitespace is a value-like 
token
+        int i = pos - 1;
+        while (i >= 0 && (input.charAt(i) == ' ' || input.charAt(i) == '\t')) {
+            i--;
+        }
+        if (i < 0) {
+            return false;
+        }
+        char prev = input.charAt(i);
+        return Character.isLetterOrDigit(prev) || prev == ')' || prev == ']' 
|| prev == '}' || prev == '"'
+                || prev == '\'';
+    }
+
+    private Token readString(char quote, int startLine, int startCol) {
+        advance(); // opening quote
+        StringBuilder sb = new StringBuilder();
+        while (pos < input.length() && input.charAt(pos) != quote) {
+            if (input.charAt(pos) == '\\' && pos + 1 < input.length()) {
+                sb.append(input.charAt(pos));
+                advance();
+                sb.append(input.charAt(pos));
+                advance();
+            } else {
+                sb.append(input.charAt(pos));
+                advance();
+            }
+        }
+        if (pos < input.length()) {
+            advance(); // closing quote
+        }
+        return new Token(TokenType.STRING, sb.toString(), startLine, startCol);
+    }
+
+    private Token readNumber(int startLine, int startCol) {
+        StringBuilder sb = new StringBuilder();
+        if (input.charAt(pos) == '-') {
+            sb.append('-');
+            advance();
+        }
+        while (pos < input.length() && (Character.isDigit(input.charAt(pos)) 
|| input.charAt(pos) == '.')) {
+            sb.append(input.charAt(pos));
+            advance();
+        }
+        return new Token(TokenType.NUMBER, sb.toString(), startLine, startCol);
+    }
+
+    private Token readIdentifier(int startLine, int startCol) {
+        StringBuilder sb = new StringBuilder();
+        while (pos < input.length() && 
(Character.isLetterOrDigit(input.charAt(pos)) || input.charAt(pos) == '_')) {
+            sb.append(input.charAt(pos));
+            advance();
+        }
+        String word = sb.toString();
+        return switch (word) {
+            case "true", "false" -> new Token(TokenType.BOOLEAN, word, 
startLine, startCol);
+            case "null" -> new Token(TokenType.NULL_LIT, word, startLine, 
startCol);
+            case "and" -> new Token(TokenType.AND, word, startLine, startCol);
+            case "or" -> new Token(TokenType.OR, word, startLine, startCol);
+            case "not" -> new Token(TokenType.NOT, word, startLine, startCol);
+            default -> new Token(TokenType.IDENTIFIER, word, startLine, 
startCol);
+        };
+    }
+
+    private Token readOperator(int startLine, int startCol) {
+        char c = input.charAt(pos);
+        advance();
+
+        return switch (c) {
+            case '+' -> {
+                if (pos < input.length() && input.charAt(pos) == '+') {
+                    advance();
+                    yield new Token(TokenType.PLUSPLUS, "++", startLine, 
startCol);
+                }
+                yield new Token(TokenType.PLUS, "+", startLine, startCol);
+            }
+            case '-' -> {
+                if (pos < input.length() && input.charAt(pos) == '>') {
+                    advance();
+                    yield new Token(TokenType.ARROW, "->", startLine, 
startCol);
+                }
+                yield new Token(TokenType.MINUS, "-", startLine, startCol);
+            }
+            case '*' -> new Token(TokenType.STAR, "*", startLine, startCol);
+            case '/' -> new Token(TokenType.SLASH, "/", startLine, startCol);
+            case '=' -> {
+                if (pos < input.length() && input.charAt(pos) == '=') {
+                    advance();
+                    yield new Token(TokenType.EQ, "==", startLine, startCol);
+                }
+                yield new Token(TokenType.EQ, "=", startLine, startCol);
+            }
+            case '!' -> {
+                if (pos < input.length() && input.charAt(pos) == '=') {
+                    advance();
+                    yield new Token(TokenType.NEQ, "!=", startLine, startCol);
+                }
+                yield new Token(TokenType.NOT, "!", startLine, startCol);
+            }
+            case '>' -> {
+                if (pos < input.length() && input.charAt(pos) == '=') {
+                    advance();
+                    yield new Token(TokenType.GE, ">=", startLine, startCol);
+                }
+                yield new Token(TokenType.GT, ">", startLine, startCol);
+            }
+            case '<' -> {
+                if (pos < input.length() && input.charAt(pos) == '=') {
+                    advance();
+                    yield new Token(TokenType.LE, "<=", startLine, startCol);
+                }
+                yield new Token(TokenType.LT, "<", startLine, startCol);
+            }
+            case '.' -> new Token(TokenType.DOT, ".", startLine, startCol);
+            case ',' -> new Token(TokenType.COMMA, ",", startLine, startCol);
+            case ':' -> new Token(TokenType.COLON, ":", startLine, startCol);
+            case ';' -> new Token(TokenType.SEMICOLON, ";", startLine, 
startCol);
+            case '(' -> new Token(TokenType.LPAREN, "(", startLine, startCol);
+            case ')' -> new Token(TokenType.RPAREN, ")", startLine, startCol);
+            case '{' -> new Token(TokenType.LBRACE, "{", startLine, startCol);
+            case '}' -> new Token(TokenType.RBRACE, "}", startLine, startCol);
+            case '[' -> new Token(TokenType.LBRACKET, "[", startLine, 
startCol);
+            case ']' -> new Token(TokenType.RBRACKET, "]", startLine, 
startCol);
+            case '$' -> new Token(TokenType.DOLLAR, "$", startLine, startCol);
+            case '%' -> new Token(TokenType.PERCENT, "%", startLine, startCol);
+            case '~' -> {
+                if (pos < input.length() && input.charAt(pos) == '=') {
+                    advance();
+                    yield new Token(TokenType.TILDE, "~=", startLine, 
startCol);
+                }
+                yield new Token(TokenType.TILDE, "~", startLine, startCol);
+            }
+            default -> {
+                // Skip unknown character
+                yield null;
+            }
+        };
+    }
+
+    private void advance() {
+        if (pos < input.length()) {
+            if (input.charAt(pos) == '\n') {
+                line++;
+                col = 1;
+            } else {
+                col++;
+            }
+            pos++;
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParser.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParser.java
new file mode 100644
index 000000000000..99cb20c0ce50
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveParser.java
@@ -0,0 +1,729 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.transform;
+
+import java.util.ArrayList;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.Token;
+import 
org.apache.camel.dsl.jbang.core.commands.transform.DataWeaveLexer.TokenType;
+
+/**
+ * Recursive descent parser for DataWeave 2.0 scripts producing {@link 
DataWeaveAst} nodes.
+ */
+public class DataWeaveParser {
+
+    private final List<Token> tokens;
+    private int pos;
+
+    public DataWeaveParser(List<Token> tokens) {
+        this.tokens = tokens;
+        this.pos = 0;
+    }
+
+    public DataWeaveAst parse() {
+        DataWeaveAst.Header header = parseHeader();
+        DataWeaveAst body = parseExpression();
+        return new DataWeaveAst.Script(header, body);
+    }
+
+    public DataWeaveAst parseExpressionOnly() {
+        return parseExpression();
+    }
+
+    // ── Header parsing ──
+
+    private DataWeaveAst.Header parseHeader() {
+        String version = "2.0";
+        String outputType = null;
+        List<DataWeaveAst.InputDecl> inputs = new ArrayList<>();
+
+        // Only parse header if it starts with %dw or a known header directive
+        boolean hasHeader = (check(TokenType.PERCENT) && peekAhead(1) != null 
&& "dw".equals(peekAhead(1).value()))
+                || checkIdentifier("output") || checkIdentifier("input");
+
+        if (!hasHeader) {
+            // No header section — skip directly to body
+            return new DataWeaveAst.Header(version, null, inputs);
+        }
+
+        // Check for %dw directive
+        if (check(TokenType.PERCENT) && peekAhead(1) != null && 
"dw".equals(peekAhead(1).value())) {
+            advance(); // %
+            advance(); // dw
+            if (check(TokenType.NUMBER)) {
+                version = current().value();
+                advance();
+            }
+        }
+
+        // Parse directives before ---
+        while (!check(TokenType.HEADER_SEPARATOR) && !check(TokenType.EOF)) {
+            if (checkIdentifier("output")) {
+                advance(); // output
+                outputType = parseMediaType();
+            } else if (checkIdentifier("input")) {
+                advance(); // input
+                String name = current().value();
+                advance();
+                String mediaType = parseMediaType();
+                inputs.add(new DataWeaveAst.InputDecl(name, mediaType));
+            } else if (checkIdentifier("import")) {
+                // Skip import directives
+                while (!check(TokenType.EOF) && !checkIdentifier("output") && 
!checkIdentifier("input")
+                        && !check(TokenType.HEADER_SEPARATOR)) {
+                    advance();
+                }
+            } else {
+                advance(); // skip unknown header tokens
+            }
+        }
+
+        if (check(TokenType.HEADER_SEPARATOR)) {
+            advance(); // ---
+        }
+
+        return new DataWeaveAst.Header(version, outputType, inputs);
+    }
+
+    private String parseMediaType() {
+        StringBuilder sb = new StringBuilder();
+        // e.g., application/json or application/xml
+        if (check(TokenType.IDENTIFIER)) {
+            sb.append(current().value());
+            advance();
+            if (check(TokenType.SLASH)) {
+                sb.append("/");
+                advance();
+                if (check(TokenType.IDENTIFIER)) {
+                    sb.append(current().value());
+                    advance();
+                }
+            }
+        }
+        return sb.toString();
+    }
+
+    // ── Expression parsing (precedence climbing) ──
+
+    private DataWeaveAst parseExpression() {
+        // Handle var/fun declarations at expression level
+        if (checkIdentifier("var")) {
+            return parseVarDecl();
+        }
+        if (checkIdentifier("fun")) {
+            return parseFunDecl();
+        }
+        if (checkIdentifier("do")) {
+            return parseDoBlock();
+        }
+        if (checkIdentifier("using")) {
+            return parseUsingBlock();
+        }
+        return parseIfElse();
+    }
+
+    private DataWeaveAst parseVarDecl() {
+        advance(); // var
+        String name = current().value();
+        advance(); // name
+        expect(TokenType.EQ); // =
+        DataWeaveAst value = parseExpression();
+        // The body follows after the var declaration (next expression in 
sequence)
+        DataWeaveAst body = null;
+        if (!check(TokenType.EOF) && !check(TokenType.RPAREN) && 
!check(TokenType.RBRACE)
+                && !check(TokenType.RBRACKET)) {
+            body = parseExpression();
+        }
+        return new DataWeaveAst.VarDecl(name, value, body);
+    }
+
+    private DataWeaveAst parseFunDecl() {
+        advance(); // fun
+        String name = current().value();
+        advance(); // name
+        expect(TokenType.LPAREN);
+        List<String> params = new ArrayList<>();
+        while (!check(TokenType.RPAREN) && !check(TokenType.EOF)) {
+            params.add(current().value());
+            advance();
+            if (check(TokenType.COMMA)) {
+                advance();
+            }
+        }
+        expect(TokenType.RPAREN);
+        expect(TokenType.EQ); // =
+        DataWeaveAst funBody = parseExpression();
+        DataWeaveAst next = null;
+        if (!check(TokenType.EOF) && !check(TokenType.RPAREN) && 
!check(TokenType.RBRACE)) {
+            next = parseExpression();
+        }
+        return new DataWeaveAst.FunDecl(name, params, funBody, next);
+    }
+
+    private DataWeaveAst parseDoBlock() {
+        advance(); // do
+        expect(TokenType.LBRACE);
+        List<DataWeaveAst> declarations = new ArrayList<>();
+        while ((checkIdentifier("var") || checkIdentifier("fun")) && 
!check(TokenType.EOF)) {
+            if (checkIdentifier("var")) {
+                advance(); // var
+                String name = current().value();
+                advance();
+                expect(TokenType.EQ);
+                DataWeaveAst value = parseExpression();
+                declarations.add(new DataWeaveAst.VarDecl(name, value, null));
+            } else {
+                declarations.add(parseFunDecl());
+            }
+        }
+        // Parse the expression part
+        if (check(TokenType.HEADER_SEPARATOR)) {
+            advance(); // ---
+        }
+        DataWeaveAst body = parseExpression();
+        expect(TokenType.RBRACE);
+        return new DataWeaveAst.Block(declarations, body);
+    }
+
+    private DataWeaveAst parseUsingBlock() {
+        advance(); // using
+        expect(TokenType.LPAREN);
+        List<DataWeaveAst> declarations = new ArrayList<>();
+        while (!check(TokenType.RPAREN) && !check(TokenType.EOF)) {
+            String name = current().value();
+            advance();
+            expect(TokenType.EQ);
+            DataWeaveAst value = parseOr();
+            declarations.add(new DataWeaveAst.VarDecl(name, value, null));
+            if (check(TokenType.COMMA)) {
+                advance();
+            }
+        }
+        expect(TokenType.RPAREN);
+        DataWeaveAst body = parseExpression();
+        return new DataWeaveAst.Block(declarations, body);
+    }
+
+    private DataWeaveAst parseIfElse() {
+        if (checkIdentifier("if")) {
+            advance(); // if
+            boolean hasParen = check(TokenType.LPAREN);
+            if (hasParen) {
+                advance();
+            }
+            DataWeaveAst condition = parseOr();
+            if (hasParen) {
+                expect(TokenType.RPAREN);
+            }
+            DataWeaveAst thenExpr = parseExpression();
+            DataWeaveAst elseExpr = null;
+            if (checkIdentifier("else")) {
+                advance();
+                elseExpr = parseExpression();
+            }
+            return new DataWeaveAst.IfElse(condition, thenExpr, elseExpr);
+        }
+        return parseDefault();
+    }
+
+    private DataWeaveAst parseDefault() {
+        DataWeaveAst expr = parseOr();
+        while (checkIdentifier("default")) {
+            advance();
+            DataWeaveAst fallback = parseOr();
+            expr = new DataWeaveAst.DefaultExpr(expr, fallback);
+        }
+        return expr;
+    }
+
+    private DataWeaveAst parseOr() {
+        DataWeaveAst left = parseAnd();
+        while (check(TokenType.OR)) {
+            advance();
+            DataWeaveAst right = parseAnd();
+            left = new DataWeaveAst.BinaryOp("or", left, right);
+        }
+        return left;
+    }
+
+    private DataWeaveAst parseAnd() {
+        DataWeaveAst left = parseComparison();
+        while (check(TokenType.AND)) {
+            advance();
+            DataWeaveAst right = parseComparison();
+            left = new DataWeaveAst.BinaryOp("and", left, right);
+        }
+        return left;
+    }
+
+    private DataWeaveAst parseComparison() {
+        DataWeaveAst left = parseConcat();
+        while (check(TokenType.EQ) || check(TokenType.NEQ) || 
check(TokenType.GT) || check(TokenType.GE)
+                || check(TokenType.LT) || check(TokenType.LE)) {
+            String op = current().value();
+            advance();
+            DataWeaveAst right = parseConcat();
+            left = new DataWeaveAst.BinaryOp(op, left, right);
+        }
+        return left;
+    }
+
+    private DataWeaveAst parseConcat() {
+        DataWeaveAst left = parseAddition();
+        while (check(TokenType.PLUSPLUS)) {
+            advance();
+            DataWeaveAst right = parseAddition();
+            left = new DataWeaveAst.BinaryOp("++", left, right);
+        }
+        return left;
+    }
+
+    private DataWeaveAst parseAddition() {
+        DataWeaveAst left = parseMultiplication();
+        while (check(TokenType.PLUS) || check(TokenType.MINUS)) {
+            String op = current().value();
+            advance();
+            DataWeaveAst right = parseMultiplication();
+            left = new DataWeaveAst.BinaryOp(op, left, right);
+        }
+        return left;
+    }
+
+    private DataWeaveAst parseMultiplication() {
+        DataWeaveAst left = parseUnary();
+        while (check(TokenType.STAR) || check(TokenType.SLASH)) {
+            String op = current().value();
+            advance();
+            DataWeaveAst right = parseUnary();
+            left = new DataWeaveAst.BinaryOp(op, left, right);
+        }
+        return left;
+    }
+
+    private DataWeaveAst parseUnary() {
+        if (check(TokenType.NOT)) {
+            advance();
+            DataWeaveAst operand = parseUnary();
+            return new DataWeaveAst.UnaryOp("not", operand);
+        }
+        if (check(TokenType.MINUS) && !isPreviousValueLike()) {
+            advance();
+            DataWeaveAst operand = parseUnary();
+            return new DataWeaveAst.UnaryOp("-", operand);
+        }
+        return parsePostfix();
+    }
+
+    private boolean isPreviousValueLike() {
+        if (pos == 0) {
+            return false;
+        }
+        Token prev = tokens.get(pos - 1);
+        return prev.type() == TokenType.IDENTIFIER || prev.type() == 
TokenType.NUMBER
+                || prev.type() == TokenType.STRING || prev.type() == 
TokenType.RPAREN
+                || prev.type() == TokenType.RBRACKET || prev.type() == 
TokenType.BOOLEAN;
+    }
+
+    private DataWeaveAst parsePostfix() {
+        DataWeaveAst expr = parsePrimary();
+        return parsePostfixOps(expr);
+    }
+
+    private DataWeaveAst parsePostfixOps(DataWeaveAst expr) {
+        while (true) {
+            if (check(TokenType.DOT)) {
+                advance(); // .
+                if (check(TokenType.STAR)) {
+                    advance(); // *
+                    String field = current().value();
+                    advance();
+                    expr = new DataWeaveAst.MultiValueSelector(expr, field);
+                } else if (check(TokenType.IDENTIFIER)) {
+                    String field = current().value();
+                    advance();
+                    expr = new DataWeaveAst.FieldAccess(expr, field);
+                }
+            } else if (check(TokenType.LBRACKET)) {
+                advance(); // [
+                DataWeaveAst index = parseExpression();
+                expect(TokenType.RBRACKET);
+                expr = new DataWeaveAst.IndexAccess(expr, index);
+            } else if (checkIdentifier("map")) {
+                advance();
+                DataWeaveAst lambda = parseLambdaOrShorthand();
+                expr = new DataWeaveAst.MapExpr(expr, lambda);
+            } else if (checkIdentifier("filter")) {
+                advance();
+                DataWeaveAst lambda = parseLambdaOrShorthand();
+                expr = new DataWeaveAst.FilterExpr(expr, lambda);
+            } else if (checkIdentifier("reduce")) {
+                advance();
+                DataWeaveAst lambda = parseLambdaOrShorthand();
+                expr = new DataWeaveAst.ReduceExpr(expr, lambda);
+            } else if (checkIdentifier("flatMap")) {
+                advance();
+                DataWeaveAst lambda = parseLambdaOrShorthand();
+                expr = new DataWeaveAst.FlatMapExpr(expr, lambda);
+            } else if (checkIdentifier("distinctBy")) {
+                advance();
+                DataWeaveAst lambda = parseLambdaOrShorthand();
+                expr = new DataWeaveAst.DistinctByExpr(expr, lambda);
+            } else if (checkIdentifier("groupBy")) {
+                advance();
+                DataWeaveAst lambda = parseLambdaOrShorthand();
+                expr = new DataWeaveAst.GroupByExpr(expr, lambda);
+            } else if (checkIdentifier("orderBy")) {
+                advance();
+                DataWeaveAst lambda = parseLambdaOrShorthand();
+                expr = new DataWeaveAst.OrderByExpr(expr, lambda);
+            } else if (checkIdentifier("as")) {
+                advance(); // as
+                String type = current().value();
+                advance();
+                String format = null;
+                if (check(TokenType.LBRACE)) {
+                    advance(); // {
+                    if (checkIdentifier("format")) {
+                        advance(); // format
+                        expect(TokenType.COLON);
+                        format = current().value();
+                        advance();
+                    }
+                    expect(TokenType.RBRACE);
+                }
+                expr = new DataWeaveAst.TypeCoercion(expr, type, format);
+            } else if (checkIdentifier("is")) {
+                advance(); // is
+                String type = current().value();
+                advance();
+                expr = new DataWeaveAst.TypeCheck(expr, type);
+            } else if (checkIdentifier("contains")) {
+                advance();
+                DataWeaveAst sub = parsePrimary();
+                expr = new DataWeaveAst.ContainsExpr(expr, sub);
+            } else if (checkIdentifier("startsWith")) {
+                advance();
+                DataWeaveAst prefix = parsePrimary();
+                expr = new DataWeaveAst.StartsWithExpr(expr, prefix);
+            } else if (checkIdentifier("endsWith")) {
+                advance();
+                DataWeaveAst suffix = parsePrimary();
+                expr = new DataWeaveAst.EndsWithExpr(expr, suffix);
+            } else if (checkIdentifier("splitBy")) {
+                advance();
+                DataWeaveAst sep = parsePrimary();
+                expr = new DataWeaveAst.SplitByExpr(expr, sep);
+            } else if (checkIdentifier("joinBy")) {
+                advance();
+                DataWeaveAst sep = parsePrimary();
+                expr = new DataWeaveAst.JoinByExpr(expr, sep);
+            } else if (checkIdentifier("replace")) {
+                advance();
+                DataWeaveAst target = parsePrimary();
+                if (checkIdentifier("with")) {
+                    advance();
+                }
+                DataWeaveAst replacement = parsePrimary();
+                expr = new DataWeaveAst.ReplaceExpr(expr, target, replacement);
+            } else {
+                break;
+            }
+        }
+        return expr;
+    }
+
+    private DataWeaveAst parseLambdaOrShorthand() {
+        // Lambda forms:
+        // ((item) -> expr)
+        // ((item, index) -> expr)
+        // ((item, acc = 0) -> expr)   (for reduce)
+        // $.field                      (shorthand)
+        // ($ -> expr)
+        if (check(TokenType.LPAREN)) {
+            int savedPos = pos;
+            try {
+                return parseLambda();
+            } catch (Exception e) {
+                // If lambda parsing fails, restore and try as expression
+                pos = savedPos;
+                return parsePrimary();
+            }
+        }
+        if (check(TokenType.DOLLAR)) {
+            return parseDollarShorthand();
+        }
+        return parsePrimary();
+    }
+
+    private DataWeaveAst parseLambda() {
+        expect(TokenType.LPAREN);
+
+        // Inner parens for parameter list: ((item) -> expr) or ((item, idx) 
-> expr)
+        boolean innerParens = check(TokenType.LPAREN);
+        if (innerParens) {
+            advance();
+        }
+
+        List<DataWeaveAst.LambdaParam> params = new ArrayList<>();
+        while (!check(TokenType.RPAREN) && !check(TokenType.ARROW) && 
!check(TokenType.EOF)) {
+            String paramName = current().value();
+            advance();
+            DataWeaveAst defaultValue = null;
+            if (check(TokenType.EQ)) {
+                advance();
+                defaultValue = parseOr();
+            }
+            params.add(new DataWeaveAst.LambdaParam(paramName, defaultValue));
+            if (check(TokenType.COMMA)) {
+                advance();
+            }
+        }
+
+        if (innerParens) {
+            expect(TokenType.RPAREN);
+        }
+
+        expect(TokenType.ARROW);
+        DataWeaveAst body = parseExpression();
+        expect(TokenType.RPAREN);
+        return new DataWeaveAst.Lambda(params, body);
+    }
+
+    private DataWeaveAst parseDollarShorthand() {
+        advance(); // $
+        List<String> fields = new ArrayList<>();
+        while (check(TokenType.DOT)) {
+            advance();
+            if (check(TokenType.IDENTIFIER)) {
+                fields.add(current().value());
+                advance();
+            }
+        }
+        return new DataWeaveAst.LambdaShorthand(fields);
+    }
+
+    private DataWeaveAst parsePrimary() {
+        if (check(TokenType.STRING)) {
+            String value = current().value();
+            advance();
+            return new DataWeaveAst.StringLit(value, false);
+        }
+
+        if (check(TokenType.NUMBER)) {
+            String value = current().value();
+            advance();
+            return new DataWeaveAst.NumberLit(value);
+        }
+
+        if (check(TokenType.BOOLEAN)) {
+            boolean value = "true".equals(current().value());
+            advance();
+            return new DataWeaveAst.BooleanLit(value);
+        }
+
+        if (check(TokenType.NULL_LIT)) {
+            advance();
+            return new DataWeaveAst.NullLit();
+        }
+
+        if (check(TokenType.DOLLAR)) {
+            return parseDollarShorthand();
+        }
+
+        if (check(TokenType.LPAREN)) {
+            advance(); // (
+            DataWeaveAst expr = parseExpression();
+            expect(TokenType.RPAREN);
+            return new DataWeaveAst.Parens(expr);
+        }
+
+        if (check(TokenType.LBRACE)) {
+            return parseObjectLiteral();
+        }
+
+        if (check(TokenType.LBRACKET)) {
+            return parseArrayLiteral();
+        }
+
+        if (check(TokenType.IDENTIFIER)) {
+            return parseIdentifierOrCall();
+        }
+
+        if (check(TokenType.MINUS)) {
+            advance();
+            DataWeaveAst operand = parsePrimary();
+            return new DataWeaveAst.UnaryOp("-", operand);
+        }
+
+        // Fallback: skip token
+        String val = current().value();
+        advance();
+        return new DataWeaveAst.Unsupported(val, "unexpected token");
+    }
+
+    private DataWeaveAst parseIdentifierOrCall() {
+        String name = current().value();
+        advance();
+
+        // Built-in function calls
+        if (check(TokenType.LPAREN)) {
+            return switch (name) {
+                case "sizeOf", "upper", "lower", "trim", "capitalize", "now", 
"uuid", "p",
+                        "isEmpty", "isBlank", "abs", "ceil", "floor", "round",
+                        "log", "sqrt", "sum", "avg", "min", "max",
+                        "read", "write", "typeOf" ->
+                    parseFunctionCall(name);
+                default -> {
+                    // Could be a custom function call or lambda
+                    // Check if it looks like a function call
+                    if (isLikelyFunctionCall()) {
+                        yield parseFunctionCall(name);
+                    }
+                    yield new DataWeaveAst.Identifier(name);
+                }
+            };
+        }
+
+        return new DataWeaveAst.Identifier(name);
+    }
+
+    private boolean isLikelyFunctionCall() {
+        // Look ahead to determine if this LPAREN starts a function call
+        // vs a lambda in a postfix operation
+        if (!check(TokenType.LPAREN)) {
+            return false;
+        }
+        int depth = 0;
+        int look = pos;
+        while (look < tokens.size()) {
+            Token t = tokens.get(look);
+            if (t.type() == TokenType.LPAREN) {
+                depth++;
+            } else if (t.type() == TokenType.RPAREN) {
+                depth--;
+                if (depth == 0) {
+                    // Check what follows the closing paren
+                    // If it's an arrow, this is a lambda, not a function call
+                    return look + 1 >= tokens.size() || tokens.get(look + 
1).type() != TokenType.ARROW;
+                }
+            } else if (t.type() == TokenType.ARROW && depth == 1) {
+                // Arrow inside first level of parens = lambda
+                return false;
+            }
+            look++;
+        }
+        return true;
+    }
+
+    private DataWeaveAst parseFunctionCall(String name) {
+        expect(TokenType.LPAREN);
+        List<DataWeaveAst> args = new ArrayList<>();
+        while (!check(TokenType.RPAREN) && !check(TokenType.EOF)) {
+            args.add(parseExpression());
+            if (check(TokenType.COMMA)) {
+                advance();
+            }
+        }
+        expect(TokenType.RPAREN);
+        return new DataWeaveAst.FunctionCall(name, args);
+    }
+
+    private DataWeaveAst parseObjectLiteral() {
+        expect(TokenType.LBRACE);
+        List<DataWeaveAst.ObjectEntry> entries = new ArrayList<>();
+
+        while (!check(TokenType.RBRACE) && !check(TokenType.EOF)) {
+            // Check for dynamic key: (expr): value
+            boolean dynamic = false;
+            DataWeaveAst key;
+            if (check(TokenType.LPAREN)) {
+                advance();
+                key = parseExpression();
+                expect(TokenType.RPAREN);
+                dynamic = true;
+            } else if (check(TokenType.IDENTIFIER)) {
+                String name = current().value();
+                advance();
+                key = new DataWeaveAst.Identifier(name);
+            } else if (check(TokenType.STRING)) {
+                key = new DataWeaveAst.StringLit(current().value(), false);
+                advance();
+            } else {
+                break;
+            }
+
+            expect(TokenType.COLON);
+            DataWeaveAst value = parseExpression();
+            entries.add(new DataWeaveAst.ObjectEntry(key, value, dynamic));
+
+            if (check(TokenType.COMMA)) {
+                advance();
+            }
+        }
+
+        expect(TokenType.RBRACE);
+        return new DataWeaveAst.ObjectLit(entries);
+    }
+
+    private DataWeaveAst parseArrayLiteral() {
+        expect(TokenType.LBRACKET);
+        List<DataWeaveAst> elements = new ArrayList<>();
+
+        while (!check(TokenType.RBRACKET) && !check(TokenType.EOF)) {
+            elements.add(parseExpression());
+            if (check(TokenType.COMMA)) {
+                advance();
+            }
+        }
+
+        expect(TokenType.RBRACKET);
+        return new DataWeaveAst.ArrayLit(elements);
+    }
+
+    // ── Token helpers ──
+
+    private Token current() {
+        return pos < tokens.size() ? tokens.get(pos) : 
tokens.get(tokens.size() - 1);
+    }
+
+    private Token peekAhead(int offset) {
+        int idx = pos + offset;
+        return idx < tokens.size() ? tokens.get(idx) : null;
+    }
+
+    private boolean check(TokenType type) {
+        return current().type() == type;
+    }
+
+    private boolean checkIdentifier(String name) {
+        return check(TokenType.IDENTIFIER) && name.equals(current().value());
+    }
+
+    private void advance() {
+        if (pos < tokens.size() - 1) {
+            pos++;
+        }
+    }
+
+    private void expect(TokenType type) {
+        if (check(type)) {
+            advance();
+        }
+        // Silently skip if not found (best-effort parsing)
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveConverterTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveConverterTest.java
new file mode 100644
index 000000000000..6dd1423b4d2d
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/transform/DataWeaveConverterTest.java
@@ -0,0 +1,393 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.transform;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+class DataWeaveConverterTest {
+
+    private DataWeaveConverter converter;
+
+    @BeforeEach
+    void setUp() {
+        converter = new DataWeaveConverter();
+    }
+
+    // ── Header conversion ──
+
+    @Test
+    void testHeaderConversion() {
+        String dw = """
+                %dw 2.0
+                output application/json
+                ---
+                { name: "test" }
+                """;
+        String result = converter.convert(dw);
+        assertTrue(result.contains("/** DataSonnet"));
+        assertTrue(result.contains("version=2.0"));
+        assertTrue(result.contains("output application/json"));
+        assertTrue(result.contains("*/"));
+    }
+
+    // ── Field access ──
+
+    @Test
+    void testPayloadToBody() {
+        String result = converter.convertExpression("payload.name");
+        assertEquals("body.name", result);
+    }
+
+    @Test
+    void testNestedPayloadAccess() {
+        String result = converter.convertExpression("payload.customer.email");
+        assertEquals("body.customer.email", result);
+    }
+
+    @Test
+    void testVarsConversion() {
+        String result = converter.convertExpression("vars.myVar");
+        assertEquals("cml.variable('myVar')", result);
+    }
+
+    @Test
+    void testAttributesHeaders() {
+        String result = 
converter.convertExpression("attributes.headers.contentType");
+        assertEquals("cml.header('contentType')", result);
+    }
+
+    @Test
+    void testAttributesQueryParams() {
+        String result = 
converter.convertExpression("attributes.queryParams.page");
+        assertEquals("cml.header('page')", result);
+    }
+
+    // ── Operators ──
+
+    @Test
+    void testStringConcat() {
+        String result = converter.convertExpression("payload.first ++ \" \" ++ 
payload.last");
+        assertEquals("body.first + \" \" + body.last", result);
+    }
+
+    @Test
+    void testArithmetic() {
+        String result = converter.convertExpression("payload.qty * 
payload.price");
+        assertEquals("body.qty * body.price", result);
+    }
+
+    @Test
+    void testComparison() {
+        String result = converter.convertExpression("payload.age >= 18");
+        assertEquals("body.age >= 18", result);
+    }
+
+    @Test
+    void testLogicalOps() {
+        String result = converter.convertExpression("payload.active and 
payload.verified");
+        assertEquals("body.active && body.verified", result);
+    }
+
+    // ── Default operator ──
+
+    @Test
+    void testDefault() {
+        String result = converter.convertExpression("payload.currency default 
\"USD\"");
+        assertEquals("cml.defaultVal(body.currency, \"USD\")", result);
+    }
+
+    // ── Type coercion ──
+
+    @Test
+    void testAsNumber() {
+        String result = converter.convertExpression("payload.count as Number");
+        assertEquals("cml.toInteger(body.count)", result);
+    }
+
+    @Test
+    void testAsString() {
+        String result = converter.convertExpression("payload.id as String");
+        assertEquals("std.toString(body.id)", result);
+    }
+
+    @Test
+    void testAsStringWithFormat() {
+        String result = converter.convertExpression("payload.date as String 
{format: \"yyyy-MM-dd\"}");
+        assertEquals("cml.formatDate(body.date, \"yyyy-MM-dd\")", result);
+    }
+
+    @Test
+    void testAsBoolean() {
+        String result = converter.convertExpression("payload.active as 
Boolean");
+        assertEquals("cml.toBoolean(body.active)", result);
+    }
+
+    // ── Built-in functions ──
+
+    @Test
+    void testSizeOf() {
+        String result = converter.convertExpression("sizeOf(payload.items)");
+        assertEquals("std.length(body.items)", result);
+    }
+
+    @Test
+    void testUpper() {
+        String result = converter.convertExpression("upper(payload.name)");
+        assertEquals("std.asciiUpper(body.name)", result);
+    }
+
+    @Test
+    void testLower() {
+        String result = converter.convertExpression("lower(payload.name)");
+        assertEquals("std.asciiLower(body.name)", result);
+    }
+
+    @Test
+    void testNow() {
+        String result = converter.convertExpression("now()");
+        assertEquals("cml.now()", result);
+    }
+
+    @Test
+    void testNowWithFormat() {
+        String result = converter.convertExpression("now() as String {format: 
\"yyyy-MM-dd\"}");
+        assertEquals("cml.nowFmt(\"yyyy-MM-dd\")", result.trim());
+    }
+
+    @Test
+    void testUuid() {
+        String result = converter.convertExpression("uuid()");
+        assertEquals("cml.uuid()", result);
+    }
+
+    @Test
+    void testP() {
+        String result = converter.convertExpression("p('config.key')");
+        assertEquals("cml.properties(\"config.key\")", result);
+    }
+
+    @Test
+    void testTrim() {
+        String result = converter.convertExpression("trim(payload.name)");
+        assertEquals("c.trim(body.name)", result);
+        assertTrue(converter.needsCamelLib());
+    }
+
+    // ── String operations ──
+
+    @Test
+    void testContains() {
+        String result = converter.convertExpression("payload.email contains 
\"@\"");
+        assertEquals("c.contains(body.email, \"@\")", result);
+        assertTrue(converter.needsCamelLib());
+    }
+
+    @Test
+    void testSplitBy() {
+        String result = converter.convertExpression("payload.tags splitBy 
\",\"");
+        assertEquals("std.split(body.tags, \",\")", result);
+    }
+
+    @Test
+    void testJoinBy() {
+        String result = converter.convertExpression("payload.items joinBy \", 
\"");
+        assertEquals("std.join(\", \", body.items)", result);
+    }
+
+    @Test
+    void testReplace() {
+        String result = converter.convertExpression("payload.text replace 
\"old\" with \"new\"");
+        assertEquals("std.strReplace(body.text, \"old\", \"new\")", result);
+    }
+
+    // ── Collection operations ──
+
+    @Test
+    void testMap() {
+        String result = converter.convertExpression(
+                "payload.items map ((item) -> { name: item.name })");
+        assertTrue(result.contains("std.map(function(item)"));
+        assertTrue(result.contains("item.name"));
+    }
+
+    @Test
+    void testFilter() {
+        String result = converter.convertExpression(
+                "payload.items filter ((item) -> item.active)");
+        assertTrue(result.contains("std.filter(function(item)"));
+        assertTrue(result.contains("item.active"));
+    }
+
+    @Test
+    void testReduce() {
+        String result = converter.convertExpression(
+                "payload.items reduce ((item, acc = 0) -> acc + item.price)");
+        assertTrue(result.contains("std.foldl(function(acc, item)"));
+        assertTrue(result.contains("acc + item.price"));
+        assertTrue(result.contains(", 0)"));
+    }
+
+    @Test
+    void testReduceParamSwap() {
+        // Verify that acc and item params are swapped for std.foldl
+        String result = converter.convertExpression(
+                "payload.items reduce ((item, acc = 0) -> acc + item.price)");
+        // In std.foldl, it should be function(acc, item) not function(item, 
acc)
+        assertTrue(result.contains("function(acc, item)"));
+    }
+
+    @Test
+    void testFlatMap() {
+        String result = converter.convertExpression(
+                "payload.items flatMap ((item) -> item.tags)");
+        assertTrue(result.contains("std.flatMap(function(item)"));
+        assertTrue(result.contains("item.tags"));
+    }
+
+    // ── If/else ──
+
+    @Test
+    void testIfElse() {
+        String result = converter.convertExpression(
+                "if (payload.age >= 18) \"adult\" else \"minor\"");
+        assertEquals("if body.age >= 18 then \"adult\" else \"minor\"", 
result);
+    }
+
+    // ── Object and array literals ──
+
+    @Test
+    void testObjectLiteral() {
+        String result = converter.convertExpression("{ name: payload.name, 
age: payload.age }");
+        assertTrue(result.contains("name: body.name"));
+        assertTrue(result.contains("age: body.age"));
+    }
+
+    @Test
+    void testArrayLiteral() {
+        String result = converter.convertExpression("[1, 2, 3]");
+        assertEquals("[1, 2, 3]", result);
+    }
+
+    // ── Full script tests ──
+
+    @Test
+    void testSimpleRenameScript() throws IOException {
+        String dw = loadResource("dataweave/simple-rename.dwl");
+        String result = converter.convert(dw);
+
+        assertTrue(result.contains("/** DataSonnet"));
+        assertTrue(result.contains("output application/json"));
+        assertTrue(result.contains("body.order_id"));
+        assertTrue(result.contains("body.customer.email"));
+        assertTrue(result.contains("body.customer.first_name + \" \" + 
body.customer.last_name"));
+        assertTrue(result.contains("cml.defaultVal(body.currency, \"USD\")"));
+        assertTrue(result.contains("status: \"RECEIVED\""));
+    }
+
+    @Test
+    void testCollectionMapScript() throws IOException {
+        String dw = loadResource("dataweave/collection-map.dwl");
+        String result = converter.convert(dw);
+
+        assertTrue(result.contains("std.map(function(item)"));
+        assertTrue(result.contains("item.product_sku"));
+        assertTrue(result.contains("cml.toInteger(item.qty)"));
+        assertTrue(result.contains("std.foldl(function(acc, item)"));
+    }
+
+    @Test
+    void testEventMessageScript() throws IOException {
+        String dw = loadResource("dataweave/event-message.dwl");
+        String result = converter.convert(dw);
+
+        assertTrue(result.contains("\"ORDER_CREATED\""));
+        assertTrue(result.contains("cml.uuid()"));
+        assertTrue(result.contains("cml.variable('correlationId')"));
+        assertTrue(result.contains("cml.variable('parsedOrder')"));
+        assertTrue(result.contains("std.length("));
+    }
+
+    @Test
+    void testTypeCoercionScript() throws IOException {
+        String dw = loadResource("dataweave/type-coercion.dwl");
+        String result = converter.convert(dw);
+
+        assertTrue(result.contains("cml.toInteger(body.count)"));
+        assertTrue(result.contains("cml.toInteger(body.total)"));
+        assertTrue(result.contains("cml.toBoolean(body.active)"));
+        assertTrue(result.contains("std.toString(body.id)"));
+        assertTrue(result.contains("cml.formatDate(body.timestamp, 
\"yyyy-MM-dd\")"));
+    }
+
+    @Test
+    void testNullHandlingScript() throws IOException {
+        String dw = loadResource("dataweave/null-handling.dwl");
+        String result = converter.convert(dw);
+
+        assertTrue(result.contains("cml.defaultVal(body.name, \"Unknown\")"));
+        assertTrue(result.contains("cml.defaultVal(body.address.city, 
\"N/A\")"));
+        assertTrue(result.contains("cml.defaultVal(body.address.country, 
\"US\")"));
+    }
+
+    @Test
+    void testStringOpsScript() throws IOException {
+        String dw = loadResource("dataweave/string-ops.dwl");
+        String result = converter.convert(dw);
+
+        assertTrue(result.contains("std.asciiUpper(body.name)"));
+        assertTrue(result.contains("std.asciiLower(body.name)"));
+        assertTrue(result.contains("c.contains(body.email, \"@\")"));
+        assertTrue(result.contains("std.split(body.tags, \",\")"));
+        assertTrue(result.contains("std.join(\"; \", body.items)"));
+        assertTrue(result.contains("std.strReplace(body.text, \"old\", 
\"new\")"));
+    }
+
+    @Test
+    void testTodoCountForUnsupportedConstructs() {
+        converter.convert("""
+                %dw 2.0
+                output application/json
+                ---
+                {
+                    value: payload.x as Date
+                }
+                """);
+        assertTrue(converter.getTodoCount() > 0, "Should have TODO count for 
unsupported 'as Date'");
+    }
+
+    @Test
+    void testNoHeaderScript() {
+        String result = converter.convert("{ name: payload.name }");
+        assertTrue(result.contains("name: body.name"));
+    }
+
+    // ── Helpers ──
+
+    private String loadResource(String path) throws IOException {
+        try (InputStream is = 
getClass().getClassLoader().getResourceAsStream(path)) {
+            assertNotNull(is, "Resource not found: " + path);
+            return new String(is.readAllBytes(), StandardCharsets.UTF_8);
+        }
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/collection-map.dwl
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/collection-map.dwl
new file mode 100644
index 000000000000..38d2d91b5caf
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/collection-map.dwl
@@ -0,0 +1,15 @@
+%dw 2.0
+output application/java
+---
+{
+    items: payload.line_items map ((item) -> {
+        sku: item.product_sku,
+        name: item.product_name,
+        quantity: item.qty as Number,
+        unitPrice: item.unit_price as Number,
+        lineTotal: (item.qty as Number) * (item.unit_price as Number)
+    }),
+    totalAmount: payload.line_items reduce ((item, acc = 0) ->
+        acc + ((item.qty as Number) * (item.unit_price as Number))
+    )
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/event-message.dwl
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/event-message.dwl
new file mode 100644
index 000000000000..d40dd4ab3a18
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/event-message.dwl
@@ -0,0 +1,18 @@
+%dw 2.0
+output application/json
+---
+{
+    eventType: "ORDER_CREATED",
+    eventId: uuid(),
+    timestamp: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'"},
+    correlationId: vars.correlationId,
+    data: {
+        orderId: vars.parsedOrder.orderId,
+        customerEmail: vars.parsedOrder.customerEmail,
+        totalAmount: vars.parsedOrder.adjustedTotal,
+        currency: vars.parsedOrder.currency,
+        itemCount: sizeOf(vars.parsedOrder.items),
+        accountTier: vars.parsedOrder.customerData.accountTier default 
"STANDARD",
+        shippingCountry: vars.parsedOrder.shippingAddress.country
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/null-handling.dwl
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/null-handling.dwl
new file mode 100644
index 000000000000..c5807e30ce9f
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/null-handling.dwl
@@ -0,0 +1,9 @@
+%dw 2.0
+output application/json
+---
+{
+    name: payload.name default "Unknown",
+    city: payload.address.city default "N/A",
+    country: payload.address.country default "US",
+    active: payload.status default "active"
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/simple-rename.dwl
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/simple-rename.dwl
new file mode 100644
index 000000000000..300c364c353c
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/simple-rename.dwl
@@ -0,0 +1,11 @@
+%dw 2.0
+output application/json
+---
+{
+    orderId: payload.order_id,
+    customerEmail: payload.customer.email,
+    customerName: payload.customer.first_name ++ " " ++ 
payload.customer.last_name,
+    currency: payload.currency default "USD",
+    orderDate: now() as String {format: "yyyy-MM-dd'T'HH:mm:ss'Z'"},
+    status: "RECEIVED"
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/string-ops.dwl 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/string-ops.dwl
new file mode 100644
index 000000000000..01fc3d0d3741
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/string-ops.dwl
@@ -0,0 +1,11 @@
+%dw 2.0
+output application/json
+---
+{
+    upper: upper(payload.name),
+    lower: lower(payload.name),
+    hasEmail: payload.email contains "@",
+    parts: payload.tags splitBy ",",
+    joined: payload.items joinBy "; ",
+    fixed: payload.text replace "old" with "new"
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/type-coercion.dwl
 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/type-coercion.dwl
new file mode 100644
index 000000000000..1f2979a6a07e
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/resources/dataweave/type-coercion.dwl
@@ -0,0 +1,10 @@
+%dw 2.0
+output application/json
+---
+{
+    count: payload.count as Number,
+    total: payload.total as Number,
+    active: payload.active as Boolean,
+    label: payload.id as String,
+    created: payload.timestamp as String {format: "yyyy-MM-dd"}
+}

Reply via email to