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;
+        }
+    }
+}

Reply via email to