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 5987a263107b CAMEL-25560: camel-yaml-dsl-validator - a key given twice 
in one mapping is reported, as YAML keeps only the last one (#27699)
5987a263107b is described below

commit 5987a263107b2c13a10a6207f3b872f60098eeb3
Author: Claus Ibsen <[email protected]>
AuthorDate: Mon Oct 12 09:19:40 2026 +0200

    CAMEL-25560: camel-yaml-dsl-validator - a key given twice in one mapping is 
reported, as YAML keeps only the last one (#27699)
    
    * CAMEL-25560: camel-yaml-dsl-validator - a key given twice in one mapping 
is reported, as YAML keeps only the last one
    
    A key written twice in one mapping passed validation: the validator reads 
the YAML into a tree where the last value
    wins, and the YAML DSL loads the route the same way, so the first value was 
lost without a word (an edit that added a
    new message: under a log: and left the old one ran with the old one). The 
validator now reports each repeated key with
    both lines, before the schema errors, from the YAML nodes, which keep both.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25560: a list key given twice, such as a when: for each branch of a 
choice, says to put the items under one key
    
    XML and Java write one when for each branch of a choice; in YAML the 
branches are the list of one when: key, and a
    second when: replaces the first. When both values are lists, the message 
says to write the key once with the - items
    of both under it.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25560: a key given three times names both lost ones, each against 
the one before it
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/dsl/yaml/validator/DuplicateKeys.java    |  85 ++++++++++++
 .../camel/dsl/yaml/validator/YamlValidator.java    |  11 +-
 .../camel/dsl/yaml/validator/DuplicateKeyTest.java | 152 +++++++++++++++++++++
 3 files changed, 247 insertions(+), 1 deletion(-)

diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/DuplicateKeys.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/DuplicateKeys.java
new file mode 100644
index 000000000000..281078d3da2d
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/DuplicateKeys.java
@@ -0,0 +1,85 @@
+/*
+ * 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.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.snakeyaml.engine.v2.nodes.MappingNode;
+import org.snakeyaml.engine.v2.nodes.Node;
+import org.snakeyaml.engine.v2.nodes.NodeTuple;
+import org.snakeyaml.engine.v2.nodes.ScalarNode;
+import org.snakeyaml.engine.v2.nodes.SequenceNode;
+
+/**
+ * CAMEL-25560: a key given twice in one mapping. YAML keeps only the last of 
the two (the tree the validator reads
+ * does, and the YAML DSL when it loads the route), so the first is lost 
without a word: an edit that adds a message:
+ * under a log: and leaves the old one runs with the old one. The YAML nodes 
still have both, with their lines.
+ */
+final class DuplicateKeys {
+
+    private DuplicateKeys() {
+    }
+
+    /** One message for each key that repeats a key of its mapping, in the 
order of the file; empty for no YAML. */
+    static List<String> find(Node root) {
+        List<String> answer = new ArrayList<>();
+        walk(root, null, answer);
+        return answer;
+    }
+
+    private static void walk(Node node, String owner, List<String> answer) {
+        if (node instanceof SequenceNode seq) {
+            for (Node item : seq.getValue()) {
+                walk(item, owner, answer);
+            }
+        } else if (node instanceof MappingNode map) {
+            Map<String, NodeTuple> seen = new HashMap<>();
+            for (NodeTuple t : map.getValue()) {
+                String key = t.getKeyNode() instanceof ScalarNode s ? 
s.getValue() : null;
+                // <<: is a merge key, which YAML allows more than once
+                if (key != null && !"<<".equals(key)) {
+                    // each one against the one before it, so with a key three 
times both lost ones are named
+                    NodeTuple previous = seen.put(key, t);
+                    if (previous != null) {
+                        answer.add(message(key, owner, previous, t));
+                    }
+                }
+                walk(t.getValueNode(), key != null ? key : owner, answer);
+            }
+        }
+    }
+
+    private static String message(String key, String owner, NodeTuple 
previous, NodeTuple again) {
+        int before = line(previous.getKeyNode());
+        int line = line(again.getKeyNode());
+        String where = owner != null ? "this " + owner + ":" : "one mapping";
+        String lost = "line " + line + ": " + key + ": is given twice in " + 
where + " (lines " + before + " and " + line
+                      + "); YAML keeps only the last one, so the " + key + ": 
at line " + before + " is lost";
+        // XML and Java write one when for each branch of a choice; YAML has 
one when: key with the branches as its list
+        if (previous.getValueNode() instanceof SequenceNode && 
again.getValueNode() instanceof SequenceNode) {
+            return lost + ": " + key + ": takes a list, so write " + key + ": 
once and put the - items of both under it";
+        }
+        return lost + ": write " + key + ": once, with the value you mean";
+    }
+
+    private static int line(Node node) {
+        return node.getStartMark().map(mark -> mark.getLine() + 1).orElse(0);
+    }
+}
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
index badbef19ab4b..55829a6d6a89 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/YamlValidator.java
@@ -198,7 +198,16 @@ public class YamlValidator {
         }
         try {
             var target = mapper.readTree(content);
-            return validate(target, bodylessEndpoints);
+            List<Error> errors = validate(target, bodylessEndpoints);
+            // CAMEL-25560: the tree keeps the last of two equal keys, so a 
key given twice is only seen in the YAML nodes
+            List<String> duplicates = 
DuplicateKeys.find(YamlPointerLines.root(content));
+            if (duplicates.isEmpty()) {
+                return errors;
+            }
+            List<Error> answer = new ArrayList<>();
+            duplicates.forEach(d -> answer.add(hint(d)));
+            answer.addAll(errors);
+            return answer;
         } catch (Exception e) {
             return List.of(parseError(e, content));
         }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/DuplicateKeyTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/DuplicateKeyTest.java
new file mode 100644
index 000000000000..1c2ab91ede6e
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/DuplicateKeyTest.java
@@ -0,0 +1,152 @@
+/*
+ * 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.util.List;
+
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * CAMEL-25560: a key given twice in one mapping is reported with both lines; 
YAML keeps only the last one, so the route
+ * would run without the first.
+ */
+public class DuplicateKeyTest {
+
+    private static YamlValidator validator;
+
+    @BeforeAll
+    public static void setup() throws Exception {
+        validator = new YamlValidator();
+        validator.init();
+    }
+
+    @Test
+    public void aMessageGivenTwiceInOneLog() {
+        // an edit added the new message under the log and left the old one: 
the route logged the old one
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - log:
+                            message: "pick ${body[qty]} x ${body[sku]} for 
${header.orderId}"
+                            message: "pick ${body[qty]} x ${body[sku]}"
+                """;
+        assertThat(lines(yaml)).singleElement().satisfies(m -> {
+            assertThat(m).contains("line 7: message: is given twice in this 
log: (lines 6 and 7)");
+            assertThat(m).contains("YAML keeps only the last one, so the 
message: at line 6 is lost");
+        });
+    }
+
+    @Test
+    public void aWhenGivenTwiceInOneChoice() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: direct:orders
+                      steps:
+                        - choice:
+                            when:
+                              - expression:
+                                  simple:
+                                    expression: "${header.country} == 'DK'"
+                                steps:
+                                  - log:
+                                      message: local
+                            when:
+                              - expression:
+                                  simple:
+                                    expression: "${header.country} == 'DE'"
+                                steps:
+                                  - log:
+                                      message: eu
+                """;
+        // a when for each branch, as XML and Java write them: in YAML the 
branches are the list of one when:
+        assertThat(lines(yaml)).singleElement().satisfies(m -> assertThat(m)
+                .contains("line 13: when: is given twice in this choice: 
(lines 6 and 13)")
+                .contains("when: takes a list, so write when: once and put the 
- items of both under it"));
+    }
+
+    @Test
+    public void aKeyGivenThreeTimesNamesBothLostOnes() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - log:
+                            message: "one"
+                            message: "two"
+                            message: "three"
+                """;
+        List<String> lines = lines(yaml);
+        assertThat(lines).hasSize(2);
+        assertThat(lines.get(0)).contains("line 7: message: is given twice in 
this log: (lines 6 and 7)")
+                .contains("the message: at line 6 is lost");
+        assertThat(lines.get(1)).contains("line 8: message: is given twice in 
this log: (lines 7 and 8)")
+                .contains("the message: at line 7 is lost");
+    }
+
+    @Test
+    public void theSameKeyInTwoStepsIsNotTwice() {
+        String yaml = """
+                - route:
+                    id: orders
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - log:
+                            id: first
+                            message: "one"
+                        - log:
+                            id: second
+                            message: "two"
+                """;
+        assertThat(lines(yaml)).isEmpty();
+    }
+
+    @Test
+    public void reportedBeforeTheSchemaErrors() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - log:
+                            message: "new"
+                            message: "old"
+                        - setBody:
+                            constant: done
+                            colour: red
+                """;
+        List<String> lines = lines(yaml);
+        assertThat(lines).hasSizeGreaterThan(1);
+        assertThat(lines.get(0)).contains("line 7: message: is given twice in 
this log: (lines 6 and 7)");
+        assertThat(lines.get(1)).contains("colour");
+    }
+
+    private static List<String> lines(String yaml) {
+        try {
+            return YamlValidator.describeAll(yaml, validator.validate(yaml));
+        } catch (Exception e) {
+            throw new AssertionError("Failed to validate:\n" + yaml, e);
+        }
+    }
+}

Reply via email to