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