This is an automated email from the ASF dual-hosted git repository.
davsclaus 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 95a406714659 CAMEL-25323: camel-dataweave strict parser + datasonnet
fail-fast conversion
95a406714659 is described below
commit 95a406714659ba0e1f2249f6834f11d4d4a0aec7
Author: Guillaume Nodet <[email protected]>
AuthorDate: Thu Oct 8 22:10:28 2026 +0200
CAMEL-25323: camel-dataweave strict parser + datasonnet fail-fast conversion
The DataWeave parser used to skip unexpected tokens silently. That
produced garbled ASTs, which then failed with cryptic DataSonnet errors
or gave wrong results. expect() now throws DataWeaveConversionException
with a line:col message. DatasonnetLanguage now fails fast when the
conversion leaves unsupported constructs, instead of logging a warning
and evaluating TODO holes as null. The camel transform dataweave CLI
reports failures per file and keeps going. An executed test corpus of
real .dwl files checks the converted output end to end. The 4.23
upgrade guide documents the stricter behaviour.
Closes #27552
Co-authored-by: Claude Sonnet 4.6 <[email protected]>
---
.../language/datasonnet/DatasonnetLanguage.java | 11 +-
.../datasonnet/DataWeaveExecutedCorpusTest.java | 221 +++++++++++++++++++++
.../src/test/resources/corpus/builtinFunctions.dwl | 8 +
.../src/test/resources/corpus/fieldAccess.dwl | 8 +
.../src/test/resources/corpus/filter.dwl | 4 +
.../src/test/resources/corpus/ifElse.dwl | 6 +
.../src/test/resources/corpus/mapTransform.dwl | 7 +
.../src/test/resources/corpus/reduce.dwl | 6 +
.../src/test/resources/corpus/stringConcat.dwl | 6 +
.../src/test/resources/corpus/typeCoercion.dwl | 7 +
.../dataweave/DataWeaveConversionException.java | 36 ++++
.../camel/component/dataweave/DataWeaveParser.java | 19 +-
.../component/dataweave/DataWeaveParserTest.java | 33 ++-
.../ROOT/pages/camel-4x-upgrade-guide-4_23.adoc | 31 +++
.../jbang/core/commands/TransformDataWeave.java | 26 ++-
.../core/commands/TransformDataWeaveTest.java | 83 ++++++++
16 files changed, 488 insertions(+), 24 deletions(-)
diff --git
a/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/DatasonnetLanguage.java
b/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/DatasonnetLanguage.java
index 8d035d4a05cd..4948bcd52b6d 100644
---
a/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/DatasonnetLanguage.java
+++
b/components/camel-datasonnet/src/main/java/org/apache/camel/language/datasonnet/DatasonnetLanguage.java
@@ -29,6 +29,7 @@ import io.github.classgraph.ClassGraph;
import io.github.classgraph.ScanResult;
import org.apache.camel.Expression;
import org.apache.camel.Predicate;
+import org.apache.camel.component.dataweave.DataWeaveConversionException;
import org.apache.camel.component.dataweave.DataWeaveConverter;
import org.apache.camel.spi.annotations.Language;
import org.apache.camel.support.LRUCacheFactory;
@@ -131,11 +132,13 @@ public class DatasonnetLanguage extends
SingleInputTypedLanguageSupport {
result = converter.convertExpression(expression);
}
if (converter.getTodoCount() > 0) {
- LOG.warn("DataWeave conversion has {} construct(s) that could not
be auto-converted and were emitted as null",
- converter.getTodoCount());
- } else {
- LOG.debug("Converted DataWeave to DataSonnet: {} expression(s)",
converter.getConvertedCount());
+ throw new DataWeaveConversionException(
+ "DataWeave expression contains " + converter.getTodoCount()
+ + " construct(s) that
cannot be auto-converted to DataSonnet."
+ + " Rewrite those
constructs in DataSonnet directly, or use a .ds script instead."
+ + " Converted expression
was: " + result);
}
+ LOG.debug("Converted DataWeave to DataSonnet: {} expression(s)",
converter.getConvertedCount());
return result;
}
diff --git
a/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/DataWeaveExecutedCorpusTest.java
b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/DataWeaveExecutedCorpusTest.java
new file mode 100644
index 000000000000..13f7c23b8041
--- /dev/null
+++
b/components/camel-datasonnet/src/test/java/org/apache/camel/language/datasonnet/DataWeaveExecutedCorpusTest.java
@@ -0,0 +1,221 @@
+/*
+ * 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.language.datasonnet;
+
+import com.datasonnet.document.MediaTypes;
+import org.apache.camel.RoutesBuilder;
+import org.apache.camel.builder.RouteBuilder;
+import org.apache.camel.component.dataweave.DataWeaveConversionException;
+import org.apache.camel.component.mock.MockEndpoint;
+import org.apache.camel.test.junit6.CamelTestSupport;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+import org.skyscreamer.jsonassert.JSONAssert;
+
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+/**
+ * Executed test corpus for DataWeave-to-DataSonnet auto-conversion
(CAMEL-25323).
+ * <p>
+ * Each test case loads a {@code .dwl} resource from {@code corpus/}, sends a
real input message through a Camel route
+ * backed by the DataSonnet language (which auto-converts the DataWeave), and
asserts the output against an expected
+ * JSON payload. This ensures that the generated DataSonnet is not only
syntactically plausible but actually runs and
+ * produces correct results.
+ * <p>
+ * A separate test verifies that routes referencing DataWeave scripts
containing unsupported constructs fail at
+ * expression creation time with a clear error rather than silently returning
{@code null}.
+ */
+class DataWeaveExecutedCorpusTest extends CamelTestSupport {
+
+ //
-------------------------------------------------------------------------
+ // Corpus cases — each sends a JSON body through a .dwl resource and checks
+ // the output JSON.
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testFieldAccess() throws Exception {
+ template.sendBody("direct:fieldAccess",
"{\"name\":\"Widget\",\"qty\":3,\"price\":5}");
+ MockEndpoint mock = getMockEndpoint("mock:fieldAccess");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+
JSONAssert.assertEquals("{\"name\":\"Widget\",\"qty\":3,\"total\":15}", result,
true);
+ }
+
+ @Test
+ void testStringConcat() throws Exception {
+ template.sendBody("direct:stringConcat",
"{\"firstName\":\"John\",\"lastName\":\"Doe\"}");
+ MockEndpoint mock = getMockEndpoint("mock:stringConcat");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+ JSONAssert.assertEquals("{\"greeting\":\"Hello, John Doe\"}", result,
true);
+ }
+
+ /**
+ * Verifies the if/else branch coverage: score ≥ 90 → A, ≥ 80 → B,
otherwise → C. A single shared {@code ifElse.dwl}
+ * script is reused across all three threshold cases.
+ */
+ @ParameterizedTest
+ @CsvSource({
+ "95, A",
+ "85, B",
+ "70, C"
+ })
+ void testIfElse(int score, String expectedLabel) throws Exception {
+ MockEndpoint mock = getMockEndpoint("mock:ifElse");
+ mock.reset();
+ template.sendBody("direct:ifElse", "{\"score\":" + score + "}");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+ JSONAssert.assertEquals("{\"label\":\"" + expectedLabel + "\"}",
result, true);
+ }
+
+ @Test
+ void testMapTransform() throws Exception {
+ template.sendBody("direct:mapTransform",
+
"{\"items\":[{\"name\":\"A\",\"price\":10},{\"name\":\"B\",\"price\":20}]}");
+ MockEndpoint mock = getMockEndpoint("mock:mapTransform");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+
JSONAssert.assertEquals("[{\"name\":\"A\",\"price\":10},{\"name\":\"B\",\"price\":20}]",
result, true);
+ }
+
+ @Test
+ void testFilter() throws Exception {
+ template.sendBody("direct:filter",
+
"{\"items\":[{\"name\":\"A\",\"qty\":1},{\"name\":\"B\",\"qty\":3},{\"name\":\"C\",\"qty\":2}]}");
+ MockEndpoint mock = getMockEndpoint("mock:filter");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+
JSONAssert.assertEquals("[{\"name\":\"B\",\"qty\":3},{\"name\":\"C\",\"qty\":2}]",
result, true);
+ }
+
+ @Test
+ void testReduce() throws Exception {
+ template.sendBody("direct:reduce", "{\"amounts\":[1,2,3,4]}");
+ MockEndpoint mock = getMockEndpoint("mock:reduce");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+ JSONAssert.assertEquals("{\"total\":10}", result, true);
+ }
+
+ @Test
+ void testBuiltinFunctions() throws Exception {
+ template.sendBody("direct:builtinFunctions", "{\"text\":\"Hello
World\"}");
+ MockEndpoint mock = getMockEndpoint("mock:builtinFunctions");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+ JSONAssert.assertEquals("{\"upper\":\"HELLO WORLD\",\"lower\":\"hello
world\",\"len\":11}", result, true);
+ }
+
+ @Test
+ void testTypeCoercion() throws Exception {
+ template.sendBody("direct:typeCoercion", "{\"amount\":\"42\"}");
+ MockEndpoint mock = getMockEndpoint("mock:typeCoercion");
+ mock.assertExchangeReceived(0);
+ String result =
mock.getExchanges().get(0).getMessage().getBody(String.class);
+ JSONAssert.assertEquals("{\"amount\":42,\"label\":\"42\"}", result,
true);
+ }
+
+ //
-------------------------------------------------------------------------
+ // Fail-fast test — unsupported construct must throw at expression
creation time
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testUnsupportedConstructFailsFast() {
+ // DataWeave 'match' is not auto-convertible;
DatasonnetLanguage.convertDataWeave()
+ // must reject the expression with a clear
DataWeaveConversionException rather than
+ // silently emitting null and letting the route start.
+ String withMatch = "%dw 2.0\noutput
application/json\n---\npayload.status match { case \"a\" -> true else -> false
}";
+ assertThrows(DataWeaveConversionException.class,
+ () -> context.resolveLanguage("datasonnet")
+ .createExpression(withMatch, new Object[] {
+ String.class, null,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE }));
+ }
+
+ //
-------------------------------------------------------------------------
+ // Strict-parser test — malformed DataWeave must report error at creation
time
+ //
-------------------------------------------------------------------------
+
+ @Test
+ void testStrictParserRejectsMalformedScript() {
+ // Inline DataWeave with a missing closing brace — the parser's strict
expect()
+ // must propagate as an exception from
DatasonnetLanguage.convertDataWeave()
+ // rather than silently producing a garbled AST.
+ String malformed = "%dw 2.0\noutput application/json\n---\n{ name:
payload.name";
+ assertThrows(DataWeaveConversionException.class,
+ () -> context.resolveLanguage("datasonnet")
+ .createExpression(malformed, new Object[] {
+ String.class, null,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE }));
+ }
+
+ //
-------------------------------------------------------------------------
+ // Route definitions
+ //
-------------------------------------------------------------------------
+
+ @Override
+ protected RoutesBuilder createRouteBuilder() {
+ return new RouteBuilder() {
+ @Override
+ public void configure() {
+ from("direct:fieldAccess")
+
.transform(datasonnet("resource:classpath:corpus/fieldAccess.dwl", String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:fieldAccess");
+
+ from("direct:stringConcat")
+
.transform(datasonnet("resource:classpath:corpus/stringConcat.dwl",
String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:stringConcat");
+
+ // Single route shared by all three ifElse threshold cases
(score >= 90 → A, >= 80 → B, else → C).
+ // The mock is reset before each @ParameterizedTest iteration
to keep assertions independent.
+ from("direct:ifElse")
+
.transform(datasonnet("resource:classpath:corpus/ifElse.dwl", String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:ifElse");
+
+ from("direct:mapTransform")
+
.transform(datasonnet("resource:classpath:corpus/mapTransform.dwl",
String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:mapTransform");
+
+ from("direct:filter")
+
.transform(datasonnet("resource:classpath:corpus/filter.dwl", String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:filter");
+
+ from("direct:reduce")
+
.transform(datasonnet("resource:classpath:corpus/reduce.dwl", String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:reduce");
+
+ from("direct:builtinFunctions")
+
.transform(datasonnet("resource:classpath:corpus/builtinFunctions.dwl",
String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:builtinFunctions");
+
+ from("direct:typeCoercion")
+
.transform(datasonnet("resource:classpath:corpus/typeCoercion.dwl",
String.class,
+ MediaTypes.APPLICATION_JSON_VALUE,
MediaTypes.APPLICATION_JSON_VALUE))
+ .to("mock:typeCoercion");
+ }
+ };
+ }
+}
diff --git
a/components/camel-datasonnet/src/test/resources/corpus/builtinFunctions.dwl
b/components/camel-datasonnet/src/test/resources/corpus/builtinFunctions.dwl
new file mode 100644
index 000000000000..1b8224915824
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/builtinFunctions.dwl
@@ -0,0 +1,8 @@
+%dw 2.0
+output application/json
+---
+{
+ upper: upper(payload.text),
+ lower: lower(payload.text),
+ len: sizeOf(payload.text)
+}
diff --git
a/components/camel-datasonnet/src/test/resources/corpus/fieldAccess.dwl
b/components/camel-datasonnet/src/test/resources/corpus/fieldAccess.dwl
new file mode 100644
index 000000000000..c3240fbb002c
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/fieldAccess.dwl
@@ -0,0 +1,8 @@
+%dw 2.0
+output application/json
+---
+{
+ name: payload.name,
+ qty: payload.qty,
+ total: payload.qty * payload.price
+}
diff --git a/components/camel-datasonnet/src/test/resources/corpus/filter.dwl
b/components/camel-datasonnet/src/test/resources/corpus/filter.dwl
new file mode 100644
index 000000000000..bfeb0135e14c
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/filter.dwl
@@ -0,0 +1,4 @@
+%dw 2.0
+output application/json
+---
+payload.items filter ((item) -> item.qty > 1)
diff --git a/components/camel-datasonnet/src/test/resources/corpus/ifElse.dwl
b/components/camel-datasonnet/src/test/resources/corpus/ifElse.dwl
new file mode 100644
index 000000000000..e3d367aad582
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/ifElse.dwl
@@ -0,0 +1,6 @@
+%dw 2.0
+output application/json
+---
+{
+ label: if (payload.score >= 90) "A" else if (payload.score >= 80) "B" else
"C"
+}
diff --git
a/components/camel-datasonnet/src/test/resources/corpus/mapTransform.dwl
b/components/camel-datasonnet/src/test/resources/corpus/mapTransform.dwl
new file mode 100644
index 000000000000..38687edebef9
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/mapTransform.dwl
@@ -0,0 +1,7 @@
+%dw 2.0
+output application/json
+---
+payload.items map ((item) -> {
+ name: item.name,
+ price: item.price
+})
diff --git a/components/camel-datasonnet/src/test/resources/corpus/reduce.dwl
b/components/camel-datasonnet/src/test/resources/corpus/reduce.dwl
new file mode 100644
index 000000000000..0100933f324f
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/reduce.dwl
@@ -0,0 +1,6 @@
+%dw 2.0
+output application/json
+---
+{
+ total: payload.amounts reduce ((item, acc = 0) -> acc + item)
+}
diff --git
a/components/camel-datasonnet/src/test/resources/corpus/stringConcat.dwl
b/components/camel-datasonnet/src/test/resources/corpus/stringConcat.dwl
new file mode 100644
index 000000000000..b70120b1958d
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/stringConcat.dwl
@@ -0,0 +1,6 @@
+%dw 2.0
+output application/json
+---
+{
+ greeting: "Hello, " ++ payload.firstName ++ " " ++ payload.lastName
+}
diff --git
a/components/camel-datasonnet/src/test/resources/corpus/typeCoercion.dwl
b/components/camel-datasonnet/src/test/resources/corpus/typeCoercion.dwl
new file mode 100644
index 000000000000..d3a10a43fff8
--- /dev/null
+++ b/components/camel-datasonnet/src/test/resources/corpus/typeCoercion.dwl
@@ -0,0 +1,7 @@
+%dw 2.0
+output application/json
+---
+{
+ amount: payload.amount as Number,
+ label: payload.amount as String
+}
diff --git
a/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConversionException.java
b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConversionException.java
new file mode 100644
index 000000000000..3bb47a39986f
--- /dev/null
+++
b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveConversionException.java
@@ -0,0 +1,36 @@
+/*
+ * 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.component.dataweave;
+
+/**
+ * Thrown when a DataWeave script cannot be converted to DataSonnet, either
because the parser encountered a syntax
+ * error or because a construct is not supported by the converter.
+ * <p>
+ * When thrown by the parser, the message includes the DataWeave source
location (line:column) and the unexpected token.
+ * When thrown for unsupported constructs detected after conversion, the
message describes the failure but does not
+ * include a source location.
+ */
+public class DataWeaveConversionException extends RuntimeException {
+
+ public DataWeaveConversionException(String message) {
+ super(message);
+ }
+
+ public DataWeaveConversionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+}
diff --git
a/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java
b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java
index b0e7ea48e8df..92508c3846b9 100644
---
a/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java
+++
b/components/camel-dataweave/src/main/java/org/apache/camel/component/dataweave/DataWeaveParser.java
@@ -683,10 +683,16 @@ public class DataWeaveParser {
return new DataWeaveAst.UnaryOp("-", operand);
}
- // Fallback: skip token
- String val = current().value();
+ // Unknown token — return an Unsupported node so best-effort callers
(e.g. the CLI
+ // `camel transform dataweave` command) can continue processing the
rest of the file.
+ // The language layer (DatasonnetLanguage) will detect the Unsupported
node via
+ // getTodoCount() and throw DataWeaveConversionException there, giving
strict behaviour
+ // at the language level while keeping the parser usable as a
best-effort migration tool.
+ Token cur = current();
+ String tokenText = cur.value() != null && !cur.value().isEmpty() ?
cur.value() : cur.type().name();
advance();
- return new DataWeaveAst.Unsupported(val, "unexpected token");
+ return new DataWeaveAst.Unsupported(
+ tokenText, "unexpected token " + cur.type() + " at " +
cur.line() + ":" + cur.col());
}
private DataWeaveAst parseIdentifierOrCall() {
@@ -884,7 +890,12 @@ public class DataWeaveParser {
private void expect(TokenType type) {
if (check(type)) {
advance();
+ } else {
+ Token cur = current();
+ throw new DataWeaveConversionException(
+ "DataWeave parse error at " + cur.line() + ":" + cur.col()
+ + ": expected " + type + "
but found " + cur.type()
+ + " ('" + cur.value() +
"')");
}
- // Silently skip if not found (best-effort parsing)
}
}
diff --git
a/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveParserTest.java
b/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveParserTest.java
index fcc71cc4e025..ceb866d50039 100644
---
a/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveParserTest.java
+++
b/components/camel-dataweave/src/test/java/org/apache/camel/component/dataweave/DataWeaveParserTest.java
@@ -38,22 +38,24 @@ import
org.apache.camel.component.dataweave.DataWeaveAst.TypeCoercion;
import org.apache.camel.component.dataweave.DataWeaveAst.UnaryOp;
import org.apache.camel.component.dataweave.DataWeaveAst.Unsupported;
import org.apache.camel.component.dataweave.DataWeaveAst.VarDecl;
-import org.apache.camel.component.dataweave.DataWeaveLexer.Token;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertInstanceOf;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
/**
* Unit tests for {@link DataWeaveParser}.
*
- * The parser is recursive descent and best-effort (it never throws on
malformed input). These tests assert the shape of
- * the produced {@link DataWeaveAst} for representative scripts, with emphasis
on operator precedence, postfix
- * collection operations, header parsing, and graceful handling of unsupported
constructs. The AST records are exercised
- * transitively here rather than in a separate test, since they carry no logic
of their own.
+ * The parser is recursive descent and strict: it throws {@link
DataWeaveConversionException} when an expected token is
+ * missing or an unexpected token is encountered. These tests assert the shape
of the produced {@link DataWeaveAst} for
+ * representative scripts, with emphasis on operator precedence, postfix
collection operations, header parsing, and
+ * correct error reporting for unsupported or malformed constructs. The AST
records are exercised transitively here
+ * rather than in a separate test, since they carry no logic of their own.
*/
class DataWeaveParserTest {
@@ -205,6 +207,17 @@ class DataWeaveParserTest {
assertEquals(List.of("name"), sh.fields());
}
+ @Test
+ void shouldFallBackToPrimaryWhenLambdaParseFailsInPostfix() {
+ // `payload map ($.price * 2)` — parseLambdaOrShorthand() sees LPAREN
and tries parseLambda().
+ // parseLambda() calls expect(ARROW) but finds STAR, so expect()
throws DataWeaveConversionException.
+ // The catch block restores pos and calls parsePrimary() instead,
yielding a Parens node.
+ // Pin this behaviour: a failed lambda parse must NOT propagate the
exception to the caller.
+ DataWeaveAst ast = parseExpr("payload map ($.price * 2)");
+ assertNotNull(ast);
+ assertInstanceOf(MapExpr.class, ast);
+ }
+
// -- Function calls --
@Test
@@ -277,10 +290,10 @@ class DataWeaveParserTest {
}
@Test
- void shouldNotThrowOnUnbalancedInput() {
- List<Token> tokens = new DataWeaveLexer("(a + b").tokenize();
- assertEquals(DataWeaveLexer.TokenType.EOF, tokens.get(tokens.size() -
1).type());
- // expect() silently skips a missing RPAREN, so a best-effort AST is
still produced
- assertInstanceOf(Parens.class, parseExpr("(a + b"));
+ void shouldThrowOnUnbalancedInput() {
+ // expect() now throws DataWeaveConversionException on a missing RPAREN
+ assertThrows(
+ DataWeaveConversionException.class,
+ () -> parseExpr("(a + b"));
}
}
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
index ae3b3396c258..16b5e5dfbb26 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_23.adoc
@@ -4945,3 +4945,34 @@ The `kubernetes-configmaps` developer console now
reports the config map names a
`camel.vault.kubernetescm` instead of reading the Kubernetes secrets vault
configuration.
Its text output labels the list `Config maps in use:` instead of `Secrets in
use:`.
Both Kubernetes vault consoles return empty lists when no resource names are
configured.
+
+=== camel-datasonnet - DataWeave auto-conversion now fails fast on unsupported
or malformed scripts
+
+`camel-datasonnet` can auto-convert DataWeave 2.0 expressions to DataSonnet at
route startup.
+Two aspects of that conversion are now stricter:
+
+* **Unsupported semantic constructs:** Previously, when the DataWeave
expression contained
+constructs that could not be auto-converted, the language emitted a `WARN` log
and continued,
+yielding a partially-converted (and likely broken) expression. Starting with
this release, such
+expressions throw `DataWeaveConversionException` at expression-creation time,
failing the route
+startup with a clear error message.
+
+* **Syntactically malformed scripts:** Previously, a DataWeave script with
syntax errors (e.g. a
+missing `}`) could pass the parser and produce a garbled or partial AST,
leading to unpredictable
+runtime behaviour. The parser now throws `DataWeaveConversionException` for
any syntactically
+invalid input, failing fast at startup instead of silently tolerating
malformed scripts.
+
+In both cases the exception is thrown at expression-creation time (route
startup), not at message
+processing time. Users who relied on the previous lenient behaviour must fix
the malformed or
+unconvertible expressions. Scripts with unsupported DataWeave constructs must
be rewritten in
+DataSonnet directly, or moved to a `.ds` file.
+
+=== camel-jbang - `camel transform dataweave` no longer aborts on parse errors
in directory mode
+
+The `camel transform dataweave` CLI command previously let a
`DataWeaveConversionException` propagate
+out of `converter.convert()`, which caused the entire batch to stop at the
first file that triggered
+a parse error (e.g. a file using conditional object elements or XML attribute
syntax that the parser
+does not yet model). The command now catches the exception per file, prints
+`<filename>: <error message>` to standard output, continues with the remaining
files, and exits
+non-zero when at least one file failed. Files that convert successfully are
still written to the
+output directory.
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
index e08ed966680f..4fb1a79af707 100644
---
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
@@ -23,6 +23,7 @@ import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
+import org.apache.camel.component.dataweave.DataWeaveConversionException;
import org.apache.camel.component.dataweave.DataWeaveConverter;
import picocli.CommandLine;
import picocli.CommandLine.Command;
@@ -73,10 +74,15 @@ public class TransformDataWeave extends CamelCommand {
converter.setIncludeComments(includeComments);
String result;
- if (expression.contains("%dw") || expression.contains("---")) {
- result = converter.convert(expression);
- } else {
- result = converter.convertExpression(expression);
+ try {
+ if (expression.contains("%dw") || expression.contains("---")) {
+ result = converter.convert(expression);
+ } else {
+ result = converter.convertExpression(expression);
+ }
+ } catch (DataWeaveConversionException e) {
+ printer().println("Error: " + e.getMessage());
+ return 1;
}
printer().println(result);
@@ -108,13 +114,21 @@ public class TransformDataWeave extends CamelCommand {
int totalTodos = 0;
int totalConverted = 0;
+ int failedFiles = 0;
for (Path dwlFile : dwlFiles) {
DataWeaveConverter converter = new DataWeaveConverter();
converter.setIncludeComments(includeComments);
String dwContent = Files.readString(dwlFile);
- String dsContent = converter.convert(dwContent);
+ String dsContent;
+ try {
+ dsContent = converter.convert(dwContent);
+ } catch (DataWeaveConversionException e) {
+ printer().println(dwlFile.getFileName() + ": " +
e.getMessage());
+ failedFiles++;
+ continue;
+ }
totalTodos += converter.getTodoCount();
totalConverted += converter.getConvertedCount();
@@ -136,7 +150,7 @@ public class TransformDataWeave extends CamelCommand {
}
printSummary(totalConverted, totalTodos, dwlFiles.size());
- return 0;
+ return failedFiles > 0 ? 1 : 0;
}
private Path resolveOutputPath(Path dwlFile, Path outputPath) {
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/TransformDataWeaveTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/TransformDataWeaveTest.java
new file mode 100644
index 000000000000..4fbe67372fff
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/TransformDataWeaveTest.java
@@ -0,0 +1,83 @@
+/*
+ * 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.nio.file.Files;
+import java.nio.file.Path;
+
+import org.apache.camel.dsl.jbang.core.common.StringPrinter;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.io.TempDir;
+import picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+class TransformDataWeaveTest {
+
+ @TempDir
+ Path dir;
+
+ /**
+ * A conditional object element ({ (a: 1) if cond }) is valid DataWeave
that the parser does not model yet; it
+ * causes expect() to throw DataWeaveConversionException. In directory
mode the CLI must report the failure for that
+ * file, continue with the remaining files, and exit non-zero.
+ */
+ @Test
+ void directoryModeReportsParseErrorPerFileAndContinues() throws Exception {
+ // valid DataWeave that converts fine
+ Path good = Files.writeString(dir.resolve("good.dwl"), """
+ %dw 2.0
+ output application/json
+ ---
+ payload
+ """);
+ // conditional object element — not yet modelled by the parser,
expect() throws
+ Path bad = Files.writeString(dir.resolve("bad.dwl"), """
+ %dw 2.0
+ output application/json
+ ---
+ { (a: 1) if payload.flag }
+ """);
+
+ StringPrinter printer = new StringPrinter();
+ TransformDataWeave cmd = new TransformDataWeave(new
CamelJBangMain().withPrinter(printer));
+ CommandLine.populateCommand(cmd, "--input=" + dir.toString());
+ int exit = cmd.doCall();
+
+ assertThat(exit).isEqualTo(1);
+ String out = printer.getOutput();
+ // the bad file's name should be reported
+ assertThat(out).contains("bad.dwl");
+ // the good file should still have been processed — multi-file header
proves it was converted
+ assertThat(out).contains("// === good.dwl ===");
+ }
+
+ /**
+ * An inline expression that the parser cannot handle returns exit code 1
with an error message.
+ */
+ @Test
+ void inlineExpressionParseErrorReturnsNonZero() throws Exception {
+ StringPrinter printer = new StringPrinter();
+ TransformDataWeave cmd = new TransformDataWeave(new
CamelJBangMain().withPrinter(printer));
+ // Unclosed parenthesis — expect() throws DataWeaveConversionException
+ CommandLine.populateCommand(cmd, "--expression=(a + b");
+ int exit = cmd.doCall();
+
+ assertThat(exit).isEqualTo(1);
+ assertThat(printer.getOutput()).contains("Error:");
+ }
+}