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 000fe342292a CAMEL-25372: camel-yaml-dsl-validator - a misplaced key 
says where it goes (#27425)
000fe342292a is described below

commit 000fe342292a730c5e8b44b6d767194e64e4a15d
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 6 11:23:13 2026 +0200

    CAMEL-25372: camel-yaml-dsl-validator - a misplaced key says where it goes 
(#27425)
    
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../camel/dsl/yaml/validator/SchemaHints.java      |  41 ++++++++-
 .../camel/dsl/yaml/validator/YamlValidator.java    |  45 +++++++++
 .../dsl/yaml/validator/StructureHintTest.java      | 101 +++++++++++++++++++++
 3 files changed, 184 insertions(+), 3 deletions(-)

diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
index 4f4b97a2bd59..5f45e6330cfd 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/SchemaHints.java
@@ -169,9 +169,44 @@ final class SchemaHints {
                     m -> "a top-level entry is one key: - route:, - beans:, - 
rest:...; the lines that belong to it"
                          + " must be indented under it, a second key at the 
same level as the entry is read as a"
                          + " separate property"),
-            append("maxProperties", ".*/steps/\\d+", ANY,
-                    m -> "a step is one EIP: an option of that EIP is indented 
under its key, and the next EIP is its"
-                         + " own - item"));
+            append("maxProperties", ".*/steps/\\d+", ANY, 
SchemaHints::stepWithTwoKeys));
+
+    /**
+     * The keys of a step after its EIP, each with where it goes: an option of 
the EIP is indented under it, another EIP
+     * is the next - item (CAMEL-25372). Without the keys, the generic 
sentence.
+     */
+    static String stepWithTwoKeys(Match m) {
+        String generic = "a step is one EIP: an option of that EIP is indented 
under its key, and the next EIP is its"
+                         + " own - item";
+        var node = m.error().getInstanceNode();
+        if (node == null || !node.isObject() || node.size() < 2) {
+            return generic;
+        }
+        List<String> keys = new ArrayList<>();
+        node.fieldNames().forEachRemaining(keys::add);
+        Set<String> steps = m.validator().stepNames();
+        String eip = 
keys.stream().filter(steps::contains).findFirst().orElse(null);
+        if (eip == null) {
+            return generic;
+        }
+        Set<String> options = m.validator().optionsOf(eip);
+        List<String> parts = new ArrayList<>();
+        for (String k : keys) {
+            if (k.equals(eip)) {
+                continue;
+            }
+            if (steps.contains(k)) {
+                parts.add(k + ": is another EIP: start it as its own item, - " 
+ k + ":");
+            } else if (options.contains(k)) {
+                parts.add(k + ": is at the column of - " + eip + ": as an 
option of " + eip + " it is indented under "
+                          + eip + ":, two spaces more, next to its other 
options");
+            } else {
+                // neither: say what a step is, without guessing where the key 
belongs
+                return generic;
+            }
+        }
+        return parts.isEmpty() ? generic : String.join("; ", parts);
+    }
 
     // 
-------------------------------------------------------------------------------------------------------------
     // expression hints
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 b8d4be27d31d..c1f60be2d4ec 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
@@ -278,6 +278,18 @@ public class YamlValidator {
             Error marked = indentationError(msg, lines);
             return marked != null ? marked : plain;
         }
+        // errorHandler: at column 1 in a file of - items is YAML, not prose: 
the list item marker is missing
+        Matcher key = TOP_KEY.matcher(text);
+        if (key.matches() && firstEntryIsListItem(lines)) {
+            String name = key.group(1);
+            return Error.builder()
+                    .messageKey("parser")
+                    .format(new MessageFormat("{0}"))
+                    .arguments("line " + line + ": " + name + ": is a 
top-level key in a file whose entries are list"
+                               + " items (- route:, ...): write it as a list 
item, - " + name + ":, and indent the lines"
+                               + " under it two spaces more, as the - route: 
entries are (CAMEL-25372)")
+                    .build();
+        }
         String cleaned = msg.replace("\n", " ").replaceAll("\\s+", " ").trim();
         int cut = cleaned.indexOf("in 'reader'");
         String head = cut > 0 ? cleaned.substring(0, cut).trim() : cleaned;
@@ -290,6 +302,21 @@ public class YamlValidator {
                 .build();
     }
 
+    /** A mapping key alone at column 1: errorHandler:, beans:, 
restConfiguration:. */
+    private static final Pattern TOP_KEY = 
Pattern.compile("^([A-Za-z][\\w-]*):\\s*$");
+
+    /** Whether the first entry of the file (past comments and blank lines) is 
a list item. */
+    private static boolean firstEntryIsListItem(String[] lines) {
+        for (String l : lines) {
+            String t = l.strip();
+            if (t.isEmpty() || t.startsWith("#") || t.equals("---")) {
+                continue;
+            }
+            return l.startsWith("- ");
+        }
+        return false;
+    }
+
     private static final Pattern SNAKE_MARK = Pattern.compile("in 'reader', 
line (\\d+), column (\\d+):");
     private static final Pattern ESCAPE_CHAR = Pattern.compile("found unknown 
escape character (.)\\(");
     private static final Pattern KEY_LINE = 
Pattern.compile("^\\s*[^-\\s#][^:]*:(\\s|$)");
@@ -1353,6 +1380,24 @@ public class YamlValidator {
         return stepNames;
     }
 
+    /** The options of an EIP as the YAML DSL schema lists them, or empty when 
the EIP is not known (CAMEL-25372). */
+    Set<String> optionsOf(String eip) {
+        JsonNode ref = 
model.at("/items/definitions/org.apache.camel.model.ProcessorDefinition/properties/"
 + eip + "/$ref");
+        JsonNode definition = ref.isTextual() ? 
model.at(ref.asText().substring(1)) : null;
+        Set<String> answer = new LinkedHashSet<>();
+        if (definition == null || definition.isMissingNode()) {
+            return answer;
+        }
+        // the definition is an object, or a oneOf of a short form and an 
object
+        List<JsonNode> shapes = new ArrayList<>();
+        shapes.add(definition);
+        definition.path("oneOf").forEach(shapes::add);
+        for (JsonNode shape : shapes) {
+            
shape.path("properties").fieldNames().forEachRemaining(answer::add);
+        }
+        return answer;
+    }
+
     /** The properties of resilience4jConfiguration, from the schema. */
     Set<String> resilienceProperties() {
         return resilienceProperties;
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/StructureHintTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/StructureHintTest.java
new file mode 100644
index 000000000000..c58171d0700a
--- /dev/null
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/StructureHintTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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-25372: the two shapes of a misplaced key, as a local model wrote 
them. A top-level key without its list item
+ * marker is YAML, not prose; a key at the column of the step's EIP says where 
it goes.
+ */
+public class StructureHintTest {
+
+    private static YamlValidator validator;
+
+    @BeforeAll
+    public static void setup() throws Exception {
+        validator = new YamlValidator();
+        validator.init();
+    }
+
+    @Test
+    public void aTopLevelKeyWithoutTheListItemMarker() {
+        String yaml = """
+                - route:
+                    id: parked
+                    from:
+                      uri: direct:parked
+                      steps:
+                        - log:
+                            message: "parked ${body}"
+
+                errorHandler:
+                  deadLetterChannel:
+                    deadLetterUri: direct:parked
+                """;
+        assertThat(lines(yaml)).singleElement().satisfies(m -> {
+            assertThat(m).contains("line 9: errorHandler: is a top-level key 
in a file whose entries are list items");
+            assertThat(m).contains("write it as a list item, - errorHandler:");
+            assertThat(m).doesNotContain("put explanations in a # comment");
+        });
+    }
+
+    @Test
+    public void anOptionAtTheColumnOfItsEip() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: file:orders
+                      steps:
+                        - to:
+                            uri: "sql:INSERT INTO customers (id) VALUES 
(:#customer)"
+                          parameters:
+                            allowNamedParameters: true
+                """;
+        assertThat(lines(yaml)).anySatisfy(m -> assertThat(m)
+                .contains("parameters: is at the column of - to: as an option 
of to it is indented under to:"));
+    }
+
+    @Test
+    public void anotherEipAtTheColumnOfTheFirst() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - log:
+                            message: "a"
+                          to:
+                            uri: log:b
+                """;
+        assertThat(lines(yaml)).anySatisfy(m -> assertThat(m)
+                .contains("to: is another EIP: start it as its own item, - 
to:"));
+    }
+
+    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