This is an automated email from the ASF dual-hosted git repository.
gnodet pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/camel.git
The following commit(s) were added to refs/heads/main by this push:
new 3eee14852dcd CAMEL-23228: Add DataWeave to DataSonnet transpiler in
camel-jbang
3eee14852dcd is described below
commit 3eee14852dcdbad89d2b060923b9ccd2260a624c
Author: Guillaume Nodet <[email protected]>
AuthorDate: Mon Mar 23 11:12:45 2026 +0100
CAMEL-23228: Add DataWeave to DataSonnet transpiler in camel-jbang
- Full DataWeave 2.0 transpiler: lexer, recursive descent parser, AST, and
DataSonnet emitter
- CLI command `camel transform dataweave` with --input (file/directory),
--expression (inline), and --output options
- Converts field access (payload→body, vars→cml.variable,
attributes.headers→cml.header)
- Converts operators (++ to +, and/or to &&/||, default to cml.defaultVal)
- Converts type coercions (as Number→cml.toDecimal, as String→std.toString,
as Boolean→cml.toBoolean, as String {format}→cml.formatDate)
- Converts collection operations (map, filter, reduce, flatMap, groupBy,
orderBy, distinctBy) with correct parameter order swaps for mapWithIndex and
foldl
- Converts string operations (contains, startsWith, endsWith, splitBy,
joinBy, replace)
- Converts built-in functions (sizeOf, upper, lower, trim, now, uuid, p,
abs, round, sqrt, avg, sum, min, max)
- Lambda shorthand ($, $.field) support
- Multi-value selector (.*field) support
- Unsupported constructs (match expressions, unknown type coercions) emit
TODO comments
- New CML.sqrt() Java-backed function for DataSonnet
- New camel.libsonnet helpers: abs, round, avg, distinctBy
- Documentation: migration section in datasonnet-language.adoc, new
function reference tables
- 55 unit tests covering expressions, operators, collection ops, full
scripts, and edge cases
- 6 DataWeave test resource files (.dwl)
---
.../src/main/docs/datasonnet-language.adoc | 50 ++
.../org/apache/camel/language/datasonnet/CML.java | 17 +
.../src/main/resources/camel.libsonnet | 12 +
.../dsl/jbang/core/commands/CamelJBangMain.java | 1 +
.../jbang/core/commands/TransformDataWeave.java | 161 +++++
.../core/commands/transform/DataWeaveAst.java | 160 +++++
.../commands/transform/DataWeaveConverter.java | 607 +++++++++++++++++
.../core/commands/transform/DataWeaveLexer.java | 333 +++++++++
.../core/commands/transform/DataWeaveParser.java | 751 +++++++++++++++++++++
.../commands/transform/DataWeaveConverterTest.java | 494 ++++++++++++++
.../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 +
pom.xml | 1 +
17 files changed, 2661 insertions(+)
diff --git a/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc
b/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc
index 2d8bd5cd74ae..15942439095b 100644
--- a/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc
+++ b/components/camel-datasonnet/src/main/docs/datasonnet-language.adoc
@@ -154,6 +154,15 @@ xref:ROOT:properties-component.adoc[Properties] component
(property placeholders
|cml.parseDate(value, format) |`cml.parseDate('20/03/2026', 'dd/MM/yyyy')`
|Parses a date string into epoch milliseconds. Returns null for null input.
|===
+=== Math Functions
+
+[width="100%",cols="20%,30%,50%",options="header",]
+|===
+|Function |Example |Description
+
+|cml.sqrt(value) |`cml.sqrt(body.x)` |Computes the square root of a number.
Returns null for null input.
+|===
+
=== Utility Functions
[width="100%",cols="20%,30%,50%",options="header",]
@@ -206,7 +215,9 @@ local c = import 'camel.libsonnet';
|`c.sumBy(arr, f)` |Sums by applying function `f` to each element.
|`c.first(arr)` / `c.last(arr)` |First/last element, or null if empty.
|`c.count(arr)` |Array length.
+|`c.avg(arr)` |Average of all elements, or null if empty.
|`c.distinct(arr)` |Removes duplicates.
+|`c.distinctBy(arr, f)` |Removes duplicates by key function (keeps first
occurrence).
|`c.flatMap(arr, f)` |Maps and flattens.
|`c.sortBy(arr, f)` |Sorts by key function.
|`c.groupBy(arr, f)` |Groups into object by key function.
@@ -215,6 +226,16 @@ local c = import 'camel.libsonnet';
|`c.zip(a, b)` |Zips two arrays into pairs.
|===
+==== Math Helpers
+
+[width="100%",cols="30%,70%",options="header",]
+|===
+|Function |Description
+
+|`c.abs(x)` |Absolute value.
+|`c.round(x)` |Rounds to nearest integer.
+|===
+
==== Object Helpers
[width="100%",cols="30%,70%",options="header",]
@@ -331,6 +352,35 @@ XML::
+== Migrating from DataWeave
+
+If you have existing DataWeave (`.dwl`) scripts, Camel provides a transpiler
to convert them to DataSonnet format via the Camel JBang CLI.
+
+=== Converting an inline expression
+
+[source,bash]
+----
+camel transform dataweave -e "payload.name ++ ' ' ++ payload.surname"
+----
+
+=== Converting a file or directory
+
+[source,bash]
+----
+camel transform dataweave --input my-transform.dwl --output my-transform.ds
+camel transform dataweave --input src/main/resources/dwl/ --output
src/main/resources/ds/
+----
+
+The transpiler automatically maps DataWeave constructs to their DataSonnet
equivalents, including:
+
+- `payload` to `body`, `vars` to `cml.variable()`, `attributes.headers` to
`cml.header()`
+- Operators: `++` to `+`, `default` to `cml.defaultVal()`, `as
Number/String/Boolean` to `cml.toInteger()/std.toString()/cml.toBoolean()`
+- Collection operations: `map`, `filter`, `reduce`, `flatMap`, `groupBy`,
`orderBy`, `distinctBy`
+- String operations: `contains`, `startsWith`, `endsWith`, `splitBy`,
`joinBy`, `replace`
+- Functions: `sizeOf`, `upper`, `lower`, `trim`, `now`, `uuid`, `abs`,
`round`, `sqrt`
+
+Constructs that cannot be automatically converted (such as `match`
expressions) are marked with `TODO` comments in the output for manual review.
+
== Dependencies
To use scripting languages in your camel routes, you need to add a
diff --git
a/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
b/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
index 50e2b2b5f9e6..0586a49dfc97 100644
---
a/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
+++
b/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/CML.java
@@ -116,6 +116,11 @@ public final class CML extends Library {
Arrays.asList("value", "format"),
params -> parseDate(params.get(0), params.get(1))));
+ // Math functions
+ answer.put("sqrt", makeSimpleFunc(
+ Collections.singletonList("value"),
+ params -> sqrt(params.get(0))));
+
// Utility functions
answer.put("uuid", makeSimpleFunc(
Collections.emptyList(),
@@ -289,6 +294,18 @@ public final class CML extends Library {
return new Val.Num(instant.toEpochMilli());
}
+ // ---- Math functions ----
+
+ private Val sqrt(Val value) {
+ if (isNull(value)) {
+ return Val.Null$.MODULE$;
+ }
+ if (value instanceof Val.Num num) {
+ return new Val.Num(Math.sqrt(num.value()));
+ }
+ throw new IllegalArgumentException("Cannot compute sqrt of " +
value.prettyName());
+ }
+
// ---- Utility functions ----
private Val uuid() {
diff --git a/components/camel-datasonnet/src/main/resources/camel.libsonnet
b/components/camel-datasonnet/src/main/resources/camel.libsonnet
index 9852088fd0b9..cfcde6df4240 100644
--- a/components/camel-datasonnet/src/main/resources/camel.libsonnet
+++ b/components/camel-datasonnet/src/main/resources/camel.libsonnet
@@ -32,6 +32,7 @@
// Collection helpers
sum(arr):: std.foldl(function(acc, x) acc + x, arr, 0),
sumBy(arr, f):: std.foldl(function(acc, x) acc + f(x), arr, 0),
+ avg(arr):: if std.length(arr) == 0 then null else std.foldl(function(acc, x)
acc + x, arr, 0) / std.length(arr),
first(arr):: if std.length(arr) > 0 then arr[0] else null,
last(arr):: if std.length(arr) > 0 then arr[std.length(arr) - 1] else null,
count(arr):: std.length(arr),
@@ -39,6 +40,13 @@
function(acc, x) if std.member(acc, x) then acc else acc + [x],
arr, []
),
+ distinctBy(arr, f):: std.foldl(
+ function(acc, x)
+ local k = f(x);
+ if std.member(acc.keys, k) then acc
+ else { keys: acc.keys + [k], items: acc.items + [x] },
+ arr, { keys: [], items: [] }
+ ).items,
flatMap(arr, f):: std.flatMap(f, arr),
sortBy(arr, f):: std.sort(arr, keyF=f),
groupBy(arr, f):: std.foldl(
@@ -57,6 +65,10 @@
take(arr, n):: arr[:n],
drop(arr, n):: arr[n:],
+ // Math helpers
+ abs(x):: if x < 0 then -x else x,
+ round(x):: std.floor(x + 0.5),
+
// Object helpers
pick(obj, keys):: { [k]: obj[k] for k in keys if std.objectHas(obj, k) },
omit(obj, keys):: { [k]: obj[k] for k in std.objectFields(obj) if
!std.member(keys, k) },
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..a7a35c740a52
--- /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 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 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..4b75a0569aff
--- /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,160 @@
+/*
+ * 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 {
+ }
+
+ // 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..3842f29b2c5e
--- /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,607 @@
+/*
+ * 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 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) {
+ // The lexer preserves escape sequences as-is, so don't double-escape
+ return "\"" + s.value().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) {
+ String collection = emitNode(mv.object());
+ return "std.map(function(x) x." + mv.field() + ", " + collection + ")";
+ }
+
+ 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.toDecimal(" + 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" -> {
+ needsCamelLib = true;
+ yield "c.abs(" + argStr + ")";
+ }
+ case "ceil" -> "std.ceil(" + argStr + ")";
+ case "floor" -> "std.floor(" + argStr + ")";
+ case "round" -> {
+ needsCamelLib = true;
+ yield "c.round(" + argStr + ")";
+ }
+ case "sqrt" -> "cml.sqrt(" + argStr + ")";
+ case "avg" -> {
+ needsCamelLib = true;
+ yield "c.avg(" + argStr + ")";
+ }
+ case "sum" -> {
+ needsCamelLib = true;
+ yield "c.sum(" + argStr + ")";
+ }
+ case "min" -> {
+ needsCamelLib = true;
+ yield "c.min(" + argStr + ")";
+ }
+ case "max" -> {
+ needsCamelLib = true;
+ yield "c.max(" + argStr + ")";
+ }
+ case "read" -> "std.parseJson(" + argStr + ")"
+ + (includeComments ? " // NOTE: assumes JSON input
— DW read() supports multiple formats" : "");
+ case "write" -> "std.manifestJsonEx(" + argStr + ", \" \")"
+ + (includeComments ? " // NOTE: outputs JSON — DW
write() supports multiple formats" : "");
+ default -> fc.name() + "(" + argStr + ")";
+ };
+ }
+
+ private String emitLambda(Lambda lam) {
+ List<String> paramNames = lambdaParamNames(lam);
+ return "function(" + String.join(", ", paramNames) + ") " +
emitNode(lam.body());
+ }
+
+ private String emitLambdaShorthand(LambdaShorthand ls) {
+ if (ls.fields().isEmpty()) {
+ return "function(x) x";
+ }
+ String path = String.join(".", ls.fields());
+ return "function(x) x." + path;
+ }
+
+ 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) {
+ // DW: map ((item, index) -> body) — DS:
std.mapWithIndex(function(index, item) body, collection)
+ // Parameter order is swapped: DW is (item, index), DS is
(index, item)
+ return "std.mapWithIndex(function(" + paramNames.get(1) + ", "
+ paramNames.get(0)
+ + ") " + 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 + ")";
+ }
+ }
+ return "std.foldl(" + emitNode(re.lambda()) + ", " + collection + ",
null)";
+ }
+
+ 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());
+ // distinctBy keeps first occurrence per key — use distinctBy
helper
+ return "c.distinctBy(" + collection + ", function(" +
paramNames.get(0) + ") " + body + ")";
+ }
+ 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) {
+ needsCamelLib = true;
+ String collection = emitNode(obe.collection());
+ if (obe.lambda() instanceof Lambda lam) {
+ List<String> paramNames = lambdaParamNames(lam);
+ String body = emitNode(lam.body());
+ return "c.sortBy(" + collection + ", function(" +
paramNames.get(0) + ") " + body + ")";
+ }
+ return "c.sortBy(" + collection + ", " + emitNode(obe.lambda()) + ")";
+ }
+
+ private String emitContains(ContainsExpr ce) {
+ needsCamelLib = true;
+ return "c.contains(" + emitNode(ce.string()) + ", " +
emitNode(ce.substring()) + ")";
+ }
+
+ private String emitStartsWith(StartsWithExpr swe) {
+ needsCamelLib = true;
+ return "c.startsWith(" + emitNode(swe.string()) + ", " +
emitNode(swe.prefix()) + ")";
+ }
+
+ private String emitEndsWith(EndsWithExpr ewe) {
+ needsCamelLib = true;
+ return "c.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 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 -> "cml.typeOf(" + expr + ") == \"" +
tc.type().toLowerCase() + "\"";
+ };
+ }
+
+ 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..1d74d45d7382
--- /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,333 @@
+/*
+ * 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;
+
+/**
+ * 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,
+ ASSIGN,
+ 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 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.ASSIGN, "=", 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..8c2f62c29dfb
--- /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,751 @@
+/*
+ * 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.ASSIGN); // =
+ 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.ASSIGN); // =
+ 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.ASSIGN);
+ 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.ASSIGN);
+ 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 if (checkIdentifier("match")) {
+ advance(); // match
+ // Capture the match block as unsupported — skip braces
+ StringBuilder matchText = new StringBuilder("match ");
+ if (check(TokenType.LBRACE)) {
+ int depth = 1;
+ matchText.append("{");
+ advance();
+ while (depth > 0 && !check(TokenType.EOF)) {
+ if (check(TokenType.LBRACE)) {
+ depth++;
+ } else if (check(TokenType.RBRACE)) {
+ depth--;
+ }
+ matchText.append(current().value());
+ if (depth > 0) {
+ matchText.append(" ");
+ }
+ advance();
+ }
+ }
+ expr = new
DataWeaveAst.Unsupported(matchText.toString().trim(), "match expression");
+ } 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.ASSIGN)) {
+ 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..c2424bd1e66a
--- /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,494 @@
+/*
+ * 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.toDecimal(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.toDecimal(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.toDecimal(body.count)"));
+ assertTrue(result.contains("cml.toDecimal(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"));
+ }
+
+ // ── startsWith / endsWith ──
+
+ @Test
+ void testStartsWith() {
+ String result = converter.convertExpression("payload.name startsWith
\"Dr\"");
+ assertEquals("c.startsWith(body.name, \"Dr\")", result);
+ assertTrue(converter.needsCamelLib());
+ }
+
+ @Test
+ void testEndsWith() {
+ String result = converter.convertExpression("payload.file endsWith
\".csv\"");
+ assertEquals("c.endsWith(body.file, \".csv\")", result);
+ assertTrue(converter.needsCamelLib());
+ }
+
+ // ── Math functions ──
+
+ @Test
+ void testAbs() {
+ String result = converter.convertExpression("abs(payload.value)");
+ assertEquals("c.abs(body.value)", result);
+ assertTrue(converter.needsCamelLib());
+ }
+
+ @Test
+ void testRound() {
+ String result = converter.convertExpression("round(payload.value)");
+ assertEquals("c.round(body.value)", result);
+ assertTrue(converter.needsCamelLib());
+ }
+
+ @Test
+ void testSqrt() {
+ String result = converter.convertExpression("sqrt(payload.value)");
+ assertEquals("cml.sqrt(body.value)", result);
+ }
+
+ @Test
+ void testAvg() {
+ String result = converter.convertExpression("avg(payload.scores)");
+ assertEquals("c.avg(body.scores)", result);
+ assertTrue(converter.needsCamelLib());
+ }
+
+ // ── mapWithIndex parameter order ──
+
+ @Test
+ void testMapWithIndex() {
+ String result = converter.convertExpression(
+ "payload.items map ((item, idx) -> { index: idx, name:
item.name })");
+ // DataSonnet std.mapWithIndex uses function(index, item), so params
must be swapped
+ assertTrue(result.contains("std.mapWithIndex(function(idx, item)"));
+ }
+
+ // ── distinctBy ──
+
+ @Test
+ void testDistinctBy() {
+ String result = converter.convertExpression(
+ "payload.items distinctBy ((item) -> item.id)");
+ assertTrue(result.contains("c.distinctBy("));
+ assertTrue(result.contains("function(item) item.id"));
+ assertTrue(converter.needsCamelLib());
+ }
+
+ // ── Lambda shorthand ──
+
+ @Test
+ void testLambdaShorthand() {
+ String result = converter.convertExpression("payload.items map
$.name");
+ assertTrue(result.contains("function(x) x.name"));
+ }
+
+ // ── match expression ──
+
+ @Test
+ void testMatchExpressionUnsupported() {
+ String result = converter.convertExpression(
+ "payload.status match { case \"active\" -> true, case
\"inactive\" -> false }");
+ assertTrue(result.contains("TODO"));
+ assertTrue(converter.getTodoCount() > 0);
+ }
+
+ // ── Multi-value selector ──
+
+ @Test
+ void testMultiValueSelector() {
+ String result = converter.convertExpression("payload.items.*name");
+ assertEquals("std.map(function(x) x.name, body.items)", result);
+ assertEquals(0, converter.getTodoCount());
+ }
+
+ // ── Escape handling ──
+
+ @Test
+ void testStringEscapesPreserved() {
+ String result = converter.convertExpression("payload.text ++ \"\\n\"");
+ assertTrue(result.contains("\"\\n\""), "Newline escape should be
preserved, got: " + result);
+ }
+
+ // ── 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"}
+}
diff --git a/pom.xml b/pom.xml
index 3932e53b30cd..5185fef9987c 100644
--- a/pom.xml
+++ b/pom.xml
@@ -267,6 +267,7 @@
<exclude>**/*.chtml</exclude>
<exclude>**/*.crt</exclude>
<exclude>**/*.csv</exclude>
+ <exclude>**/*.dwl</exclude>
<exclude>**/*.event</exclude>
<exclude>**/*.gif</exclude>
<exclude>**/*.gpg</exclude>