This is an automated email from the ASF dual-hosted git repository. davsclaus pushed a commit to branch feature/CAMEL-25206-yaml-round-trip in repository https://gitbox.apache.org/repos/asf/camel.git
commit 8e7b13dcfbac42ad8a97b36c279518855708c742 Author: Claus Ibsen <[email protected]> AuthorDate: Wed Sep 30 21:39:33 2026 +0200 CAMEL-25206: camel-yaml-dsl - round-trip test of the YAML DSL writer and reader over the XML route corpus The XML routes of camel-xml-io and camel-core's model tests are dumped as YAML, loaded back with the YAML DSL, and both models compared as XML. 124 of 133 routes read back; the test found two gaps, fixed here: - route properties: the YAML dumper writes routeProperty, the YAML DSL did not read it. - XML namespaces read from YAML (namespace list) lost their order, as NamespaceAwareExpression collected them in a HashMap. The routes left differ by design: in YAML an EIP with no steps of its own (kamelet, saga, transacted) takes the steps after it, as in the Java DSL, and interceptFrom inside a route is read as the interceptFrom of all routes. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]> Signed-off-by: Claus Ibsen <[email protected]> --- .../camel/catalog/schemas/camelYamlDsl-model.json | 5 + .../model/language/NamespaceAwareExpression.java | 3 +- .../deserializers/RouteDefinitionDeserializer.java | 6 + dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml | 5 + .../resources/schema/camelYamlDsl-canonical.json | 7 ++ .../resources/schema/camelYamlDsl-model.json | 5 + .../generated/resources/schema/camelYamlDsl.json | 7 ++ .../apache/camel/dsl/yaml/YamlRoundTripTest.java | 122 +++++++++++++++++++++ 8 files changed, 158 insertions(+), 2 deletions(-) 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 320e9d67ea9f..8ead60dc0333 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-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/pom.xml b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml index 341553734b81..ec67b9942724 100644 --- a/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/pom.xml @@ -72,6 +72,11 @@ <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.apache.camel</groupId> <artifactId>camel-main</artifactId> 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 320e9d67ea9f..8ead60dc0333 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.", diff --git a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/YamlRoundTripTest.java b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/YamlRoundTripTest.java new file mode 100644 index 000000000000..eb0018445099 --- /dev/null +++ b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/YamlRoundTripTest.java @@ -0,0 +1,122 @@ +/* + * 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; + +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 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). + */ +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, where the XML routes of the corpus keep them apart. + */ + private static final Map<String, String> KNOWN = Map.of( + "kamelet.xml", "the steps after kamelet are its own in YAML", + "saga.xml", "the steps after saga are its own in YAML", + "transacted.xml", "the steps after transacted are its own in YAML", + "interceptFrom.xml", "interceptFrom inside a route: YAML reads it as the interceptFrom of all the routes"); + + 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); + xml = new LwModelToXMLDumper().dumpModelAsXml(context, route); + } + 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; + } + } +}
