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 45a18f9f9824 CAMEL-25206: camel-yaml-dsl - round-trip test of the YAML
DSL writer and reader over the XML route corpus (#27157)
45a18f9f9824 is described below
commit 45a18f9f9824a24ef06db4523f3c8aa1678acd12
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 1 07:06:08 2026 +0200
CAMEL-25206: camel-yaml-dsl - round-trip test of the YAML DSL writer and
reader over the XML route corpus (#27157)
The XML and Java DSL writers have round-trip tests over route corpora; the
YAML DSL had none (`XmlToYamlTest` in camel-yaml-io only checks the YAML is not
empty).
`YamlRoundTripTest` (in camel-yaml-dsl-validator, which has the YAML reader
and Camel's `YamlValidator`) reads the XML routes of camel-xml-io and
camel-core's model tests, and for each one:
1. dumps it as YAML (`LwModelToYAMLDumper`);
2. validates that YAML with `YamlValidator`, the checks of `camel validate`;
3. loads it back with the YAML DSL;
4. compares both models dumped as XML. The XML side is prepared the way the
routes loader prepares the route it reads back (a flat `<transacted/>` or
`<saga/>` takes the rest of the route; an `interceptFrom` that does not match
the route is removed).
camel-yaml-io and camel-xml-io are added as test dependencies of
camel-yaml-dsl-validator.
### Fixed
- **Route properties**: the YAML dumper writes `routeProperty`, but the
YAML DSL rejected it (`unsupported field: routeProperty`). The route
deserializer now reads it; the YAML schemas and the catalog copy are
regenerated.
- **XML namespace order**: namespaces read from a YAML `namespace` list
lost their order because `NamespaceAwareExpression.getNamespaceAsMap()`
collected them in a `HashMap`. It is now a `LinkedHashMap`, as the copy
constructor already uses.
- **Kamelet EIP doc**: the XML example put the steps after `<kamelet>`,
where the Java and YAML examples nest them (they run at the kamelet's sink).
The XML example now nests them too.
### Left out
- `kamelet.xml`: a flat `<kamelet/>` runs the steps after it once the
kamelet returns; in YAML they are the kamelet's own (flow mode, as in the Java
DSL).
- Route scoped `onException`, `onCompletion`, `intercept`, `interceptFrom`
and `interceptSendToEndpoint` (5 routes): the YAML DSL has them at the top
level or in a route configuration, not inside a route, so the YAML written for
them does not validate. Adding a route scope to YAML was judged overkill
([CAMEL-25207](https://issues.apache.org/jira/browse/CAMEL-25207), Won't Do).
128 of 133 routes read back.
Tests: camel-yaml-dsl, camel-yaml-dsl-validator (168), camel-core-model,
camel-xml-io, camel-yaml-io, camel-java-io all pass.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Signed-off-by: Claus Ibsen <[email protected]>
---
.../org/apache/camel/catalog/docs/kamelet-eip.adoc | 5 +-
.../camel/catalog/schemas/camelYamlDsl-model.json | 5 +
.../main/docs/modules/eips/pages/kamelet-eip.adoc | 5 +-
.../model/language/NamespaceAwareExpression.java | 3 +-
.../deserializers/RouteDefinitionDeserializer.java | 6 +
.../camel-yaml-dsl-validator/pom.xml | 10 ++
.../dsl/yaml/validator/YamlRoundTripTest.java | 139 +++++++++++++++++++++
.../resources/schema/camelYamlDsl-canonical.json | 7 ++
.../resources/schema/camelYamlDsl-model.json | 5 +
.../generated/resources/schema/camelYamlDsl.json | 7 ++
10 files changed, 186 insertions(+), 6 deletions(-)
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-eip.adoc
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-eip.adoc
index a241edeb52b6..16c97466223d 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-eip.adoc
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-eip.adoc
@@ -169,8 +169,9 @@ XML::
----
<route>
<from uri="direct:start"/>
- <kamelet name="foo"/>
- <to uri="mock:result"/>
+ <kamelet name="foo">
+ <to uri="mock:result"/>
+ </kamelet>
</route>
----
diff --git
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
index 5962bfc01ece..5f661bb07177 100644
---
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
+++
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/schemas/camelYamlDsl-model.json
@@ -7054,6 +7054,11 @@
"type" : "object",
"ref" : "from",
"required" : true
+ }, {
+ "name" : "routeProperty",
+ "type" : "array",
+ "ref" : "property",
+ "description" : "To set metadata as properties on the route."
} ]
},
"routeTemplate" : {
diff --git
a/core/camel-core-engine/src/main/docs/modules/eips/pages/kamelet-eip.adoc
b/core/camel-core-engine/src/main/docs/modules/eips/pages/kamelet-eip.adoc
index a241edeb52b6..16c97466223d 100644
--- a/core/camel-core-engine/src/main/docs/modules/eips/pages/kamelet-eip.adoc
+++ b/core/camel-core-engine/src/main/docs/modules/eips/pages/kamelet-eip.adoc
@@ -169,8 +169,9 @@ XML::
----
<route>
<from uri="direct:start"/>
- <kamelet name="foo"/>
- <to uri="mock:result"/>
+ <kamelet name="foo">
+ <to uri="mock:result"/>
+ </kamelet>
</route>
----
diff --git
a/core/camel-core-model/src/main/java/org/apache/camel/model/language/NamespaceAwareExpression.java
b/core/camel-core-model/src/main/java/org/apache/camel/model/language/NamespaceAwareExpression.java
index fc85ec7a6994..4859617b74fe 100644
---
a/core/camel-core-model/src/main/java/org/apache/camel/model/language/NamespaceAwareExpression.java
+++
b/core/camel-core-model/src/main/java/org/apache/camel/model/language/NamespaceAwareExpression.java
@@ -17,7 +17,6 @@
package org.apache.camel.model.language;
import java.util.ArrayList;
-import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
@@ -104,7 +103,7 @@ public abstract class NamespaceAwareExpression extends
SingleInputTypedExpressio
public Map<String, String> getNamespaceAsMap() {
if (namespaces == null && namespace != null) {
- namespaces = new HashMap<>();
+ namespaces = new LinkedHashMap<>();
}
if (namespace != null) {
for (PropertyDefinition def : namespace) {
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteDefinitionDeserializer.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteDefinitionDeserializer.java
index fa6d7e04cd2d..77e0c2c91f21 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteDefinitionDeserializer.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-deserializers/src/main/java/org/apache/camel/dsl/yaml/deserializers/RouteDefinitionDeserializer.java
@@ -24,6 +24,7 @@ import org.apache.camel.model.ErrorHandlerDefinition;
import org.apache.camel.model.FromDefinition;
import org.apache.camel.model.InputTypeDefinition;
import org.apache.camel.model.OutputTypeDefinition;
+import org.apache.camel.model.PropertyDefinition;
import org.apache.camel.model.RouteDefinition;
import org.apache.camel.spi.annotations.YamlIn;
import org.apache.camel.spi.annotations.YamlProperty;
@@ -67,6 +68,8 @@ import org.snakeyaml.engine.v2.nodes.NodeTuple;
description = "To control how to shut down the
route."),
@YamlProperty(name = "inputType", type =
"object:org.apache.camel.model.InputTypeDefinition"),
@YamlProperty(name = "outputType", type =
"object:org.apache.camel.model.OutputTypeDefinition"),
+ @YamlProperty(name = "routeProperty", type =
"array:org.apache.camel.model.PropertyDefinition",
+ description = "To set metadata as properties
on the route."),
@YamlProperty(name = "from", type =
"object:org.apache.camel.model.FromDefinition", required = true)
})
public class RouteDefinitionDeserializer extends
YamlDeserializerBase<RouteDefinition> {
@@ -162,6 +165,9 @@ public class RouteDefinitionDeserializer extends
YamlDeserializerBase<RouteDefin
case "outputType":
target.setOutputType(asType(val,
OutputTypeDefinition.class));
break;
+ case "routeProperty":
+ target.setRouteProperties(asFlatList(val,
PropertyDefinition.class));
+ break;
case "from":
val.setProperty(RouteDefinition.class.getName(), target);
target.setInput(asType(val, FromDefinition.class));
diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml
index 4392c785b763..ab54795fb6f2 100644
--- a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml
+++ b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/pom.xml
@@ -64,6 +64,16 @@
<version>${networknt-json-schema-validator-version}</version>
</dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-yaml-io</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.apache.camel</groupId>
+ <artifactId>camel-xml-io</artifactId>
+ <scope>test</scope>
+ </dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlRoundTripTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlRoundTripTest.java
new file mode 100644
index 000000000000..2900bf70bf68
--- /dev/null
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlRoundTripTest.java
@@ -0,0 +1,139 @@
+/*
+ * 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.yaml.validator;
+
+import java.io.InputStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Stream;
+
+import com.networknt.schema.Error;
+import org.apache.camel.impl.DefaultCamelContext;
+import org.apache.camel.model.ModelCamelContext;
+import org.apache.camel.model.RouteDefinition;
+import org.apache.camel.model.RoutesDefinition;
+import org.apache.camel.support.PluginHelper;
+import org.apache.camel.support.ResourceHelper;
+import org.apache.camel.xml.LwModelToXMLDumper;
+import org.apache.camel.xml.in.ModelParser;
+import org.apache.camel.yaml.LwModelToYAMLDumper;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.junit.jupiter.api.Assumptions.assumeTrue;
+
+/**
+ * The routes of the XML test corpus of camel-xml-io are dumped as YAML and
loaded back with the YAML DSL: both models,
+ * dumped as XML, must be the same, so what the YAML dumper writes is what the
YAML DSL reads (CAMEL-25206). The YAML
+ * must also pass the checks of camel validate.
+ */
+class YamlRoundTripTest {
+
+ /** XML routes of the core modules' tests: camel-xml-io's parser tests and
camel-core's model tests. */
+ private static final List<Path> CORPUS = List.of(
+ Path.of("../../../core/camel-xml-io/src/test/resources"),
+
Path.of("../../../core/camel-core/src/test/resources/org/apache/camel/model"));
+
+ /**
+ * Routes that read back differently, and why. In YAML an EIP with no
steps of its own takes the steps after it, as
+ * in the Java DSL; the XML routes of the corpus keep them apart. The YAML
DSL has no route scoped onException,
+ * onCompletion or interceptors (they go at the top level or in a
routeConfiguration): the YAML written for them
+ * does not validate.
+ */
+ private static final Map<String, String> KNOWN = Map.of(
+ "kamelet.xml", "the steps after kamelet are its own in YAML: they
run at the kamelet's sink",
+ "barOnExceptionRoute.xml", "route scoped onException: YAML has it
at the top level or in a routeConfiguration",
+ "onCompletion.xml", "route scoped onCompletion: YAML has it at the
top level or in a routeConfiguration",
+ "barInterceptorRoute.xml", "route scoped intercept: YAML has it at
the top level or in a routeConfiguration",
+ "interceptFrom.xml", "route scoped interceptFrom: YAML has it at
the top level or in a routeConfiguration",
+ "interceptFromAndSendTo.xml",
+ "route scoped interceptSendToEndpoint: YAML has it at the top
level or in a routeConfiguration");
+
+ private static List<RouteDefinition> routes(Path file) {
+ for (String ns : List.of("http://camel.apache.org/schema/xml-io",
"http://camel.apache.org/schema/spring")) {
+ try (InputStream in = Files.newInputStream(file)) {
+ RoutesDefinition routes = new ModelParser(in,
ns).parseRoutesDefinition().orElse(null);
+ if (routes != null && !routes.getRoutes().isEmpty()) {
+ return routes.getRoutes();
+ }
+ } catch (Exception e) {
+ // not a routes file in this namespace
+ }
+ }
+ return List.of();
+ }
+
+ @Test
+ void xmlCorpus() throws Exception {
+ assumeTrue(Files.isDirectory(CORPUS.get(0)), "the core test routes are
in the source tree");
+ List<Path> files = new ArrayList<>();
+ for (Path dir : CORPUS) {
+ try (Stream<Path> s = Files.list(dir)) {
+ files.addAll(s.filter(p ->
p.toString().endsWith(".xml")).sorted().toList());
+ }
+ }
+ List<String> failures = new ArrayList<>();
+ int routes = 0;
+ for (Path file : files) {
+ for (RouteDefinition route : routes(file)) {
+ routes++;
+ String problem = roundTrip(route);
+ if (problem != null &&
!KNOWN.containsKey(file.getFileName().toString())) {
+ failures.add(file.getFileName() + "\n" + problem);
+ }
+ }
+ }
+ assertThat(routes).isGreaterThan(100);
+ assertThat(failures).as("%d of %d routes do not read back:%n%s",
failures.size(), routes,
+ String.join("\n\n", failures)).isEmpty();
+ }
+
+ /** Null when the route reads back as it was, else what went wrong. */
+ private static String roundTrip(RouteDefinition route) throws Exception {
+ String yaml;
+ String xml;
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+ yaml = new LwModelToYAMLDumper().dumpModelAsYaml(context, route);
+ // prepared as the routes loader prepares the route read back
(transacted and saga take the route,
+ // an interceptFrom that does not match the route is removed)
+ RoutesDefinition routes = new RoutesDefinition();
+ routes.setCamelContext(context);
+ routes.prepareRoute(route);
+ xml = new LwModelToXMLDumper().dumpModelAsXml(context, route);
+ }
+ // the YAML written must pass the checks of camel validate, not only
load
+ List<Error> errors = new YamlValidator().validate(yaml);
+ if (!errors.isEmpty()) {
+ return yaml + "\n--- does not validate: " + errors;
+ }
+ try (DefaultCamelContext context = new DefaultCamelContext()) {
+
PluginHelper.getRoutesLoader(context).loadRoutes(ResourceHelper.fromString("route.yaml",
yaml));
+ List<RouteDefinition> loaded =
context.getCamelContextExtension().getContextPlugin(ModelCamelContext.class)
+ .getRouteDefinitions();
+ if (loaded.size() != 1) {
+ return yaml + "\n--- loads " + loaded.size() + " routes";
+ }
+ String back = new LwModelToXMLDumper().dumpModelAsXml(context,
loaded.get(0));
+ return back.equals(xml) ? null : yaml + "\n--- was ---\n" + xml +
"\n--- read back as ---\n" + back;
+ } catch (Exception e) {
+ return yaml + "\n--- does not load: " + e;
+ }
+ }
+}
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
index 86b49fb70073..fb088be1f633 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-canonical.json
@@ -4131,6 +4131,13 @@
"routePolicyRef" : {
"type" : "string"
},
+ "routeProperty" : {
+ "type" : "array",
+ "description" : "To set metadata as properties on the route.",
+ "items" : {
+ "$ref" :
"#/items/definitions/org.apache.camel.model.PropertyDefinition"
+ }
+ },
"shutdownRoute" : {
"type" : "string",
"description" : "To control how to shut down the route.",
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
index 5962bfc01ece..5f661bb07177 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl-model.json
@@ -7054,6 +7054,11 @@
"type" : "object",
"ref" : "from",
"required" : true
+ }, {
+ "name" : "routeProperty",
+ "type" : "array",
+ "ref" : "property",
+ "description" : "To set metadata as properties on the route."
} ]
},
"routeTemplate" : {
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
index 8561e8fbf1b5..2a2ff1970e27 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/generated/resources/schema/camelYamlDsl.json
@@ -5408,6 +5408,13 @@
"routePolicyRef" : {
"type" : "string"
},
+ "routeProperty" : {
+ "type" : "array",
+ "description" : "To set metadata as properties on the route.",
+ "items" : {
+ "$ref" :
"#/items/definitions/org.apache.camel.model.PropertyDefinition"
+ }
+ },
"shutdownRoute" : {
"type" : "string",
"description" : "To control how to shut down the route.",