This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch feature/CAMEL-25381-normalize-kamelets
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 8998b8cb9cc54f94d08a4798ae211f1f63e5d7cf
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 17:40:31 2026 +0200

    CAMEL-25381: camel-jbang - camel validate normalize supports Kamelet files
    
    A Kamelet file is loaded as a route template and its spec.template is 
replaced
    by the canonical YAML DSL of that template; the rest of the file (license,
    metadata, definition, dependencies, comments) is kept as written. Route
    templates of other files are no longer dropped from the output.
    
    The YAML dump writes a kamelet: endpoint with the Kamelet in the uri
    (kamelet:log-sink) instead of a templateId parameter, and the YAML DSL keeps
    the order of map entries such as bean properties as written.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../camel/yaml/out/YamlModelWriterSupport.java     |  11 +-
 .../core/commands/validate/KameletNormalizer.java  | 268 +++++++++++++++++++++
 .../commands/validate/YamlNormalizeCommand.java    |  66 ++++-
 .../validate/YamlNormalizeKameletTest.java         | 161 +++++++++++++
 .../kamelets/tag-order-action.kamelet.yaml         |  62 +++++
 .../dsl/yaml/common/YamlDeserializerSupport.java   |   6 +-
 .../camel/dsl/yaml/PipeLoaderErrorHandlerTest.java |   3 +-
 7 files changed, 567 insertions(+), 10 deletions(-)

diff --git 
a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java
 
b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java
index 6af0454bb719..4cf9af451515 100644
--- 
a/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java
+++ 
b/core/camel-yaml-io/src/main/java/org/apache/camel/yaml/out/YamlModelWriterSupport.java
@@ -230,7 +230,16 @@ public abstract class YamlModelWriterSupport {
                 if (idx != -1) {
                     scheme = scheme.substring(0, idx);
                 }
-                jo.put("uri", scheme);
+                if ("kamelet".equals(scheme) && params.get("templateId") != 
null) {
+                    // the Kamelet is named in the uri (kamelet:log-sink, 
kamelet:source), not as a templateId
+                    // parameter: that is how Kamelets are written and read
+                    params = new LinkedHashMap<>(params);
+                    String path = params.remove("templateId");
+                    String routeId = params.remove("routeId");
+                    jo.put("uri", "kamelet:" + path + (routeId != null ? "/" + 
routeId : ""));
+                } else {
+                    jo.put("uri", scheme);
+                }
             }
             if (params != null && !params.isEmpty()) {
                 JsonObject p = new JsonObject();
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/KameletNormalizer.java
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/KameletNormalizer.java
new file mode 100644
index 000000000000..910e8a3dfb45
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/KameletNormalizer.java
@@ -0,0 +1,268 @@
+/*
+ * 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.validate;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/**
+ * Normalizes a Kamelet file: its spec.template is replaced by the canonical 
YAML DSL of the route template the Kamelet
+ * loads as, taken from a route template dump, and the rest of the file 
(metadata, definition, dependencies, comments)
+ * is kept as written.
+ */
+final class KameletNormalizer {
+
+    private static final Pattern TEMPLATE_ID = Pattern.compile("^ {4}id: 
\"?([^\"\\s]+)\"?\\s*$");
+
+    private KameletNormalizer() {
+    }
+
+    /** Whether the content is a Kamelet: a document of kind Kamelet. */
+    static boolean isKamelet(String content) {
+        return content != null && 
Pattern.compile("(?m)^kind:\\s*Kamelet\\s*$").matcher(content).find();
+    }
+
+    /** The name of the Kamelet: metadata.name, else the file name without 
.kamelet.yaml. */
+    static String kameletName(String content, String fileName) {
+        boolean inMetadata = false;
+        for (String line : content.split("\n", -1)) {
+            if (line.startsWith("metadata:")) {
+                inMetadata = true;
+            } else if (inMetadata && !line.isBlank() && !line.startsWith(" ") 
&& !line.startsWith("#")) {
+                inMetadata = false;
+            } else if (inMetadata) {
+                Matcher m = Pattern.compile("^ 
{2}name:\\s*\"?([^\"\\s]+)\"?\\s*$").matcher(line);
+                if (m.find()) {
+                    return m.group(1);
+                }
+            }
+        }
+        String name = fileName.replace('\\', '/');
+        name = name.substring(name.lastIndexOf('/') + 1);
+        return name.endsWith(".kamelet.yaml") ? name.substring(0, 
name.length() - ".kamelet.yaml".length()) : name;
+    }
+
+    /**
+     * The Kamelet with its spec.template replaced by the beans and route of 
the route template of the same id in the
+     * dump, or null when the dump has no such template or the Kamelet has no 
spec.template.
+     */
+    static String normalize(String kamelet, String dump, String templateId) {
+        List<String> chunk = templateChunk(dump, templateId);
+        if (chunk == null) {
+            return null;
+        }
+        String[] lines = kamelet.split("\n", -1);
+        int spec = -1;
+        for (int i = 0; i < lines.length; i++) {
+            if (lines[i].matches("^spec:\\s*$")) {
+                spec = i;
+                break;
+            }
+        }
+        if (spec < 0) {
+            return null;
+        }
+        // the keys of spec are at the indent of its first key; template is 
one of them
+        int template = -1;
+        int templateIndent = -1;
+        for (int i = spec + 1; i < lines.length; i++) {
+            String l = lines[i];
+            if (l.isBlank() || l.trim().startsWith("#")) {
+                continue;
+            }
+            int indent = indent(l);
+            if (indent == 0) {
+                break;
+            }
+            if (templateIndent < 0) {
+                templateIndent = indent;
+            }
+            if (indent == templateIndent && 
l.trim().matches("^template:\\s*$")) {
+                template = i;
+                break;
+            }
+        }
+        if (template < 0) {
+            return null;
+        }
+        int end = lines.length;
+        for (int i = template + 1; i < lines.length; i++) {
+            String l = lines[i];
+            if (l.isBlank()) {
+                continue;
+            }
+            if (indent(l) <= templateIndent) {
+                end = i;
+                break;
+            }
+        }
+        // a template written as route: keeps that key, else from:
+        boolean routeKey = false;
+        for (int i = template + 1; i < end; i++) {
+            if (lines[i].trim().startsWith("route:") && indent(lines[i]) > 
templateIndent) {
+                routeKey = true;
+                break;
+            }
+            if (lines[i].trim().startsWith("from:") && indent(lines[i]) > 
templateIndent) {
+                break;
+            }
+        }
+        // the children of template at the indent the file uses for them
+        int child = templateIndent + 2;
+        for (int i = template + 1; i < end; i++) {
+            if (!lines[i].isBlank() && !lines[i].trim().startsWith("#")) {
+                child = indent(lines[i]);
+                break;
+            }
+        }
+        List<String> body = new ArrayList<>();
+        // the dump: "- routeTemplate:" at 0, its keys at 4 (id, parameters, 
beans, route), route's from at 6
+        List<String> beans = block(chunk, "beans:", 4);
+        if (beans != null) {
+            body.addAll(shift(beans, child - 4));
+        }
+        List<String> route = block(chunk, "route:", 4);
+        if (route == null) {
+            return null;
+        }
+        if (routeKey) {
+            body.addAll(shift(route, child - 4));
+        } else {
+            List<String> from = block(route, "from:", 6);
+            if (from == null) {
+                return null;
+            }
+            body.addAll(shift(from, child - 6));
+        }
+        // trailing blank lines of the template block belong to what follows it
+        int bodyEnd = end;
+        while (bodyEnd > template + 1 && lines[bodyEnd - 1].isBlank()) {
+            bodyEnd--;
+        }
+        List<String> out = new ArrayList<>();
+        for (int i = 0; i <= template; i++) {
+            out.add(lines[i]);
+        }
+        out.addAll(body);
+        for (int i = bodyEnd; i < lines.length; i++) {
+            out.add(lines[i]);
+        }
+        return String.join("\n", out);
+    }
+
+    /** The dump without the routeTemplates of the given ids: what is left are 
the routes of the other files. */
+    static String withoutTemplates(String dump, Set<String> ids) {
+        if (ids.isEmpty()) {
+            return dump;
+        }
+        List<String> out = new ArrayList<>();
+        List<String> item = new ArrayList<>();
+        boolean drop = false;
+        for (String l : dump.split("\n", -1)) {
+            if (l.startsWith("- ")) {
+                if (!drop) {
+                    out.addAll(item);
+                }
+                item = new ArrayList<>();
+                drop = false;
+            }
+            item.add(l);
+            Matcher m = TEMPLATE_ID.matcher(l);
+            if (!item.isEmpty() && item.get(0).startsWith("- routeTemplate:") 
&& m.find() && ids.contains(m.group(1))) {
+                drop = true;
+            }
+        }
+        if (!drop) {
+            out.addAll(item);
+        }
+        return String.join("\n", out).strip();
+    }
+
+    /** The lines of the routeTemplate with the given id in the dump, or null. 
*/
+    static List<String> templateChunk(String dump, String templateId) {
+        if (dump == null) {
+            return null;
+        }
+        List<String> current = null;
+        boolean match = false;
+        for (String l : dump.split("\n", -1)) {
+            if (l.startsWith("- routeTemplate:")) {
+                if (match) {
+                    return current;
+                }
+                current = new ArrayList<>();
+                match = false;
+            }
+            if (current != null) {
+                current.add(l);
+                Matcher m = TEMPLATE_ID.matcher(l);
+                if (m.find() && m.group(1).equals(templateId)) {
+                    match = true;
+                }
+            }
+        }
+        return match ? current : null;
+    }
+
+    /** The key line at the indent and the lines under it, or null when the 
key is not there. */
+    static List<String> block(List<String> lines, String key, int indent) {
+        List<String> answer = null;
+        for (String l : lines) {
+            if (answer == null) {
+                if (indent(l) == indent && l.trim().equals(key)) {
+                    answer = new ArrayList<>();
+                    answer.add(l);
+                }
+            } else if (l.isBlank() || indent(l) > indent) {
+                answer.add(l);
+            } else {
+                break;
+            }
+        }
+        if (answer != null) {
+            while (!answer.isEmpty() && answer.get(answer.size() - 
1).isBlank()) {
+                answer.remove(answer.size() - 1);
+            }
+        }
+        return answer;
+    }
+
+    private static List<String> shift(List<String> lines, int by) {
+        List<String> answer = new ArrayList<>();
+        for (String l : lines) {
+            if (l.isBlank()) {
+                answer.add("");
+            } else if (by >= 0) {
+                answer.add(" ".repeat(by) + l);
+            } else {
+                answer.add(l.substring(Math.min(-by, indent(l))));
+            }
+        }
+        return answer;
+    }
+
+    private static int indent(String line) {
+        int i = 0;
+        while (i < line.length() && line.charAt(i) == ' ') {
+            i++;
+        }
+        return i;
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java
index 38fb2a0c80e0..3b54f4ef1e2e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/main/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeCommand.java
@@ -20,8 +20,12 @@ import java.io.InputStream;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
 import java.util.Stack;
 
 import org.apache.camel.dsl.jbang.core.commands.CamelCommand;
@@ -71,6 +75,14 @@ public class YamlNormalizeCommand extends CamelCommand {
         if (matched.isEmpty()) {
             return 0;
         }
+        // a Kamelet loads as a route template: its template is normalized and 
put back into the Kamelet file
+        Map<String, String> kamelets = new LinkedHashMap<>();
+        for (String n : matched) {
+            String content = readFile(n);
+            if (content != null && KameletNormalizer.isKamelet(content)) {
+                kamelets.put(n, content);
+            }
+        }
 
         String dump = CommandLineHelper.CAMEL_JBANG_WORK_DIR + 
"/normalize-output.yaml";
         Files.deleteIfExists(Path.of(dump));
@@ -80,7 +92,8 @@ public class YamlNormalizeCommand extends CamelCommand {
             @Override
             protected void doAddInitialProperty(KameletMain main) {
                 main.addInitialProperty("camel.main.dumpRoutes", "yaml");
-                main.addInitialProperty("camel.main.dumpRoutesInclude", 
"routes,rests,routeConfigurations,beans,dataFormats");
+                main.addInitialProperty("camel.main.dumpRoutesInclude",
+                        
"routes,rests,routeConfigurations,routeTemplates,beans,dataFormats");
                 main.addInitialProperty("camel.main.dumpRoutesLog", "false");
                 
main.addInitialProperty("camel.main.dumpRoutesResolvePlaceholders", "false");
                 
main.addInitialProperty("camel.main.dumpRoutesUriAsParameters", "true");
@@ -101,24 +114,67 @@ public class YamlNormalizeCommand extends CamelCommand {
 
         String normalized = waitForDumpFile(Path.of(target));
         if (normalized == null) {
-            printer().printErr("Error normalizing files");
+            printer().printErr("Error normalizing files: nothing was loaded 
from " + String.join(", ", matched)
+                               + " (see the errors above)");
             return 1;
         }
 
+        // the dump has one item per routeTemplate, route, rest...: those of 
the Kamelets go back into their files
+        Map<String, String> kameletOutput = new LinkedHashMap<>();
+        Set<String> kameletIds = new HashSet<>();
+        for (Map.Entry<String, String> e : kamelets.entrySet()) {
+            String id = KameletNormalizer.kameletName(e.getValue(), 
e.getKey());
+            String k = KameletNormalizer.normalize(e.getValue(), normalized, 
id);
+            if (k == null) {
+                printer().printErr("Error normalizing the Kamelet " + 
e.getKey() + ": its template " + id
+                                   + " did not load (see the errors above), or 
the file has no spec.template");
+                return 1;
+            }
+            kameletIds.add(id);
+            kameletOutput.put(e.getKey(), k);
+        }
+        String routes = KameletNormalizer.withoutTemplates(normalized, 
kameletIds);
+
+        List<String> documents = new ArrayList<>();
+        if (!routes.isBlank()) {
+            documents.add(routes);
+        }
+        documents.addAll(kameletOutput.values());
         if (output != null) {
             Path outPath = Path.of(output);
             if (Files.isDirectory(outPath)) {
-                outPath = outPath.resolve("normalized.yaml");
+                if (!routes.isBlank()) {
+                    Files.writeString(outPath.resolve("normalized.yaml"), 
routes);
+                }
+                for (Map.Entry<String, String> e : kameletOutput.entrySet()) {
+                    
Files.writeString(outPath.resolve(Path.of(e.getKey()).getFileName()), 
e.getValue());
+                }
+            } else if (documents.size() > 1 && !kameletOutput.isEmpty()) {
+                printer().printErr("--output must be a directory to normalize 
Kamelets with other files:"
+                                   + " each Kamelet is written to a file of 
its own name");
+                return 1;
+            } else {
+                Files.writeString(outPath, documents.get(0));
             }
-            Files.writeString(outPath, normalized);
             printer().println("Normalized " + matched.size() + " file(s) to " 
+ output);
         } else {
-            printer().println(normalized);
+            // more than one document: one YAML stream, separated by ---
+            printer().println(String.join("\n---\n", documents));
         }
 
         return 0;
     }
 
+    private static String readFile(String name) {
+        try {
+            String n = name.startsWith("file:") ? name.substring(5) : name;
+            Path p = Path.of(n);
+            return Files.isRegularFile(p) ? Files.readString(p) : null;
+        } catch (Exception e) {
+            return null;
+        }
+    }
+
     private String waitForDumpFile(Path dumpFile) {
         StopWatch watch = new StopWatch();
         while (watch.taken() < 5000) {
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeKameletTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeKameletTest.java
new file mode 100644
index 000000000000..c106d232db15
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/java/org/apache/camel/dsl/jbang/core/commands/validate/YamlNormalizeKameletTest.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.validate;
+
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.util.Set;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+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;
+
+/**
+ * CAMEL-25381: camel validate normalize on a Kamelet file normalizes its 
template and keeps the rest of the file.
+ */
+class YamlNormalizeKameletTest {
+
+    private static final Path KAMELET = 
Path.of("src/test/resources/kamelets/tag-order-action.kamelet.yaml");
+    private static final Path ROUTE = Path.of("src/test/resources/route.yaml");
+
+    private static StringPrinter printer;
+
+    private static int normalize(String... args) throws Exception {
+        printer = new StringPrinter();
+        YamlNormalizeCommand cmd = new YamlNormalizeCommand(new 
CamelJBangMain().withPrinter(printer));
+        CommandLine.populateCommand(cmd, args);
+        return cmd.doCall();
+    }
+
+    @Test
+    void aKameletIsNormalizedInItsTemplateOnly(@TempDir Path out) throws 
Exception {
+        assertThat(normalize("--output=" + out, KAMELET.toString())).isZero();
+        String original = Files.readString(KAMELET);
+        String normalized = 
Files.readString(out.resolve("tag-order-action.kamelet.yaml"));
+
+        // the license, metadata and definition (with its comment) as written, 
and what follows the template
+        assertThat(normalized).startsWith(original.substring(0, 
original.indexOf("  template:\n") + 12));
+        assertThat(normalized).endsWith(original.substring(original.indexOf("  
dependencies:")));
+        // the template in canonical form: expressions under expression:, 
endpoints as uri:
+        String template = normalized.substring(normalized.indexOf("  
template:\n"), normalized.indexOf("  dependencies:"));
+        assertThat(template.replaceAll("\\s+", " "))
+                .contains("- setBody: expression: simple: expression: 
\"${body} [{{tag}}]\"")
+                .contains("- expression: simple: expression: 
\"${header.priority} == true\"");
+        assertThat(template).contains("uri: kamelet:source").contains("uri: 
kamelet:sink")
+                .doesNotContain("templateId");
+        // the bean properties in the order they were written
+        
assertThat(template.indexOf("zeta:")).isLessThan(template.indexOf("alpha:"));
+    }
+
+    @Test
+    void aKameletIsPrintedWhenThereIsNoOutput() throws Exception {
+        assertThat(normalize(KAMELET.toString())).isZero();
+        assertThat(printer.getOutput()).contains("kind: 
Kamelet").contains("name: tag-order-action")
+                .doesNotContain("routeTemplate");
+    }
+
+    @Test
+    void aKameletAndARouteAreTwoDocuments() throws Exception {
+        assertThat(normalize(KAMELET.toString(), ROUTE.toString())).isZero();
+        String output = printer.getOutput();
+        assertThat(output).contains("\n---\n").contains("kind: 
Kamelet").contains("- route:")
+                .doesNotContain("routeTemplate");
+    }
+
+    @Test
+    void theTemplateIsReplacedInTheKameletText() {
+        String kamelet = """
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: my-action
+                spec:
+                  definition:
+                    title: Mine
+                  template:
+                    from:
+                      uri: kamelet:source
+                      steps:
+                        - setBody:
+                            constant: hi
+
+                  types:
+                    out:
+                      mediaType: text/plain
+                """;
+        String dump = """
+                - routeTemplate:
+                    id: other
+                    route:
+                      from:
+                        uri: timer
+                - routeTemplate:
+                    id: my-action
+                    parameters:
+                      - name: x
+                    route:
+                      from:
+                        uri: kamelet:source
+                        steps:
+                          - setBody:
+                              expression:
+                                constant:
+                                  expression: hi
+                - route:
+                    from:
+                      uri: direct
+                """;
+        String normalized = KameletNormalizer.normalize(kamelet, dump, 
"my-action");
+        assertThat(normalized).isEqualTo("""
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: my-action
+                spec:
+                  definition:
+                    title: Mine
+                  template:
+                    from:
+                      uri: kamelet:source
+                      steps:
+                        - setBody:
+                            expression:
+                              constant:
+                                expression: hi
+
+                  types:
+                    out:
+                      mediaType: text/plain
+                """);
+        assertThat(KameletNormalizer.withoutTemplates(dump, 
Set.of("my-action")))
+                .contains("id: other").contains("- 
route:").doesNotContain("my-action");
+        assertThat(KameletNormalizer.normalize(kamelet, dump, 
"missing")).isNull();
+    }
+
+    @Test
+    void theNameOfAKamelet() {
+        assertThat(KameletNormalizer.kameletName("kind: Kamelet\nmetadata:\n  
name: abc\nspec:\n", "x.kamelet.yaml"))
+                .isEqualTo("abc");
+        assertThat(KameletNormalizer.kameletName("kind: Kamelet\nspec:\n", 
"dir/xyz.kamelet.yaml")).isEqualTo("xyz");
+        assertThat(KameletNormalizer.isKamelet("apiVersion: 
camel.apache.org/v1\nkind: Kamelet\n")).isTrue();
+        assertThat(KameletNormalizer.isKamelet("- route:\n    
from:\n")).isFalse();
+    }
+}
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/kamelets/tag-order-action.kamelet.yaml
 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/kamelets/tag-order-action.kamelet.yaml
new file mode 100644
index 000000000000..ee57b86262fd
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-validate/src/test/resources/kamelets/tag-order-action.kamelet.yaml
@@ -0,0 +1,62 @@
+#
+# 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.
+#
+
+apiVersion: camel.apache.org/v1
+kind: Kamelet
+metadata:
+  name: tag-order-action
+  labels:
+    camel.apache.org/kamelet.type: action
+spec:
+  definition:
+    title: Tag Order
+    # the tag is what the agent passes
+    required:
+      - tag
+    type: object
+    properties:
+      tag:
+        title: Tag
+        type: string
+      priority:
+        title: Priority
+        type: boolean
+        default: false
+  template:
+    beans:
+      - name: tagger
+        type: "#class:java.util.HashMap"
+        properties:
+          zeta: "{{tag}}"
+          alpha: "{{priority}}"
+    from:
+      uri: kamelet:source
+      steps:
+        - choice:
+            when:
+              - simple: "${header.priority} == true"
+                steps:
+                  - setBody:
+                      simple: "${body} [!{{tag}}]"
+            otherwise:
+              steps:
+                - setBody:
+                    simple: "${body} [{{tag}}]"
+        - to: kamelet:sink
+  dependencies:
+    - "camel:core"
+    - "camel:kamelet"
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java
index 4b2bef9d6b5d..b2baa35c1c7e 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-common/src/main/java/org/apache/camel/dsl/yaml/common/YamlDeserializerSupport.java
@@ -20,8 +20,8 @@ import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Base64;
 import java.util.Collection;
-import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.LinkedHashSet;
 import java.util.List;
 import java.util.Map;
@@ -179,7 +179,7 @@ public class YamlDeserializerSupport {
         }
 
         final MappingNode mn = asMappingNode(node);
-        final Map<String, Object> answer = new HashMap<>();
+        final Map<String, Object> answer = new LinkedHashMap<>();
 
         for (NodeTuple tuple : mn.getValue()) {
             final String key = asText(tuple.getKeyNode());
@@ -206,7 +206,7 @@ public class YamlDeserializerSupport {
         }
 
         final MappingNode mn = asMappingNode(node);
-        final Map<String, Object> answer = new HashMap<>();
+        final Map<String, Object> answer = new LinkedHashMap<>();
 
         for (NodeTuple tuple : mn.getValue()) {
             final String key = asText(tuple.getKeyNode());
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java
index a11cd9f8ad01..ce9530549e32 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl/src/test/java/org/apache/camel/dsl/yaml/PipeLoaderErrorHandlerTest.java
@@ -81,8 +81,9 @@ class PipeLoaderErrorHandlerTest extends YamlTestSupport {
         assertThat(route.getErrorHandlerFactory()).isNotNull();
         
assertThat(route.getErrorHandlerFactory()).isInstanceOf(DeadLetterChannelDefinition.class);
         DeadLetterChannelDefinition eh = (DeadLetterChannelDefinition) 
route.getErrorHandlerFactory();
+        // the properties in the order the Pipe declares them
         assertThat(eh.getDeadLetterUri()).isEqualTo(
-                
"kamelet:error-handler?kafkaTopic=my-first-test&logMessage=ERROR!&kafkaServiceAccountId=scott&kafkaBrokers=my-broker&kafkaServiceAccountSecret=tiger");
+                
"kamelet:error-handler?logMessage=ERROR!&kafkaBrokers=my-broker&kafkaTopic=my-first-test&kafkaServiceAccountId=scott&kafkaServiceAccountSecret=tiger");
         
assertThat(eh.getRedeliveryPolicy().getMaximumRedeliveries()).isEqualTo("1");
         
assertThat(eh.getRedeliveryPolicy().getRedeliveryDelay()).isEqualTo("2000");
         assertThat(route.getRouteId()).isEqualTo("timer-event-source");

Reply via email to