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

davsclaus pushed a commit to branch fix/CAMEL-25870
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 2d7d61ed500630d05231c959980725341c516cc3
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 09:18:41 2026 +0200

    CAMEL-25870: camel-yaml-dsl-validator - say where a language goes, 
translate a key in the column of a list, - stop, and keep JSON in the constant 
hint
    
    - A language written as a step (- simple: "${body}") says where an 
expression goes, instead of "did you mean 'sample'?".
    - "expected <block end>, but found '?'" (a key in the column of the dashes 
of a list) names the column of the item's
      keys and the column of the key that holds the list, one of which was 
meant.
    - A step written as a plain name (- stop) says to write it as a key: - 
stop: {}, or - to: {uri: "..."}.
    - The hints for a ${...} in a constant or in another language quote the 
text as written, in single quotes when it holds
      a double quote, so {"error": ...} is no longer turned into {'error': 
...}, which is not JSON.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../camel/dsl/yaml/validator/SchemaHints.java      | 28 +++++++
 .../camel/dsl/yaml/validator/YamlValidator.java    | 56 ++++++++++++-
 .../dsl/yaml/validator/ConstantWithSimpleTest.java |  3 +-
 .../validator/YamlValidatorPropertyHintTest.java   | 94 ++++++++++++++++++++++
 4 files changed, 177 insertions(+), 4 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 f73aaf52c2e9..91e852f297a8 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
@@ -435,6 +435,23 @@ final class SchemaHints {
                         return "write one or more of " + (values == null ? 
"the listed values" : values.replace("|", ", "))
                                + ", separated by comma";
                     }),
+            // - stop as a step: a step is a key with its options as a map, {} 
when it has none to set
+            replace("type", ".*/steps/\\d+",
+                    m -> m.message().contains("string found, object expected") 
&& m.error().getInstanceNode() != null
+                            && m.error().getInstanceNode().isTextual(),
+                    m -> {
+                        String text = 
m.error().getInstanceNode().asText().trim();
+                        if (!m.validator().stepNames().contains(text)) {
+                            return "a step is a key followed by its options, 
such as - log: {message: \"...\"} or"
+                                   + " - stop: {}; " + text + " is not a step";
+                        }
+                        String property = 
SchemaHints.STRING_STEP_PROPERTY.get(text);
+                        return "a step is a key followed by its options: write 
- " + text + ": "
+                               + (property != null
+                                       ? "{" + property + ": \"...\"}"
+                                       : "{} (the options of the step, if any, 
go inside the braces)");
+                    },
+                    "type", "type"),
             append("type", "/?", m -> m.message().contains("array expected"),
                     m -> "a Camel YAML file is a list of entries, each 
starting with \"- \": - route:, - from:, - beans:,"
                          + " - rest:, - onException:"),
@@ -618,6 +635,17 @@ final class SchemaHints {
                             && !m.error().getInstanceNode().has("expression")
                             && 
m.error().getInstanceNode().path(m.unknown()).isTextual(),
                     m -> languageTextHint(m.name(), m.unknown(), valueOf(m, 
m.unknown(), "..."))),
+            // - simple: "${body}" as a step: a language is not a step (and no 
"did you mean 'sample'?")
+            unknownProperty(".*/steps/\\d+",
+                    m -> m.validator().languageKeys().contains(m.unknown())
+                            && 
!m.validator().stepNames().contains(m.unknown()),
+                    m -> {
+                        String text = YamlValidator.yamlQuoted(valueOf(m, 
m.unknown(), "..."));
+                        return "'" + m.unknown() + "' is a language, not a 
step: an expression is written in a step that"
+                               + " uses it, such as - setBody: {expression: {" 
+ m.unknown() + ": {expression: " + text
+                               + "}}} to set the body, or - filter: 
{expression: {" + m.unknown() + ": {expression: "
+                               + text + "}}, steps: [...]} to keep the 
messages it matches";
+                    }),
             unknownProperty(null,
                     m -> YamlValidator.closest(m.unknown(), 
m.validator().knownProperties(m.schemaLocation())) != null,
                     m -> "did you mean '"
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 4552f458b1e4..d51ac3e72cb8 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
@@ -390,6 +390,10 @@ public class YamlValidator {
         if (msg.contains("expected <block end>, but found '<block mapping 
start>'")) {
             return marks.size() > 1 ? mappingKeyColumn(marks.get(0), problem, 
lines) : null;
         }
+        // a key in the column of the dashes of a list: '?' is the parser's 
name for a key
+        if (msg.contains("expected <block end>, but found '?'")) {
+            return marks.size() > 1 ? keyInListColumn(marks.get(0), problem, 
lines) : null;
+        }
         if (msg.contains("mapping values are not allowed here")) {
             return mappingValue(problem, lines);
         }
@@ -499,6 +503,45 @@ public class YamlValidator {
                     + " are in column " + mapping.column() + "; every key of a 
mapping must start in the same column");
     }
 
+    /**
+     * A key in the column of the dashes of the list above it, such as steps: 
under - simple: of a when: list, or
+     * onFallback: under the - to: of the steps: of a circuitBreaker: it is 
part of neither the list item nor the
+     * mapping that holds the list. Names both columns, one of which was meant.
+     */
+    private static Error keyInListColumn(Mark item, Mark problem, String[] 
lines) {
+        String itemLine = lines[item.line() - 1];
+        int dash = item.column() - 1;
+        if (dash < 0 || dash >= itemLine.length() || itemLine.charAt(dash) != 
'-') {
+            return null;
+        }
+        String afterDash = itemLine.substring(dash + 1);
+        int itemKeyColumn = dash + 2 + afterDash.length() - 
afterDash.stripLeading().length();
+        String key = keyName(lines[problem.line() - 1]);
+        String answer = "line " + problem.line() + ": " + key + " starts in 
column " + problem.column()
+                        + ", the column of the - of the list item at line " + 
item.line() + ", so it is part of neither"
+                        + " that item nor the key that holds the list. Write 
it in column " + itemKeyColumn
+                        + " to make it a key of the item, as " + 
keyName(afterDash) + " is";
+        // the key whose value the list is: the nearest line above that is 
indented less than the dashes
+        for (int i = item.line() - 2; i >= 0; i--) {
+            String stripped = lines[i].stripLeading();
+            if (stripped.isEmpty() || stripped.startsWith("#")) {
+                continue;
+            }
+            int indent = lines[i].length() - stripped.length();
+            if (indent < dash) {
+                int column = indent + 1;
+                if (stripped.startsWith("-")) {
+                    String rest = stripped.substring(1);
+                    stripped = rest.stripLeading();
+                    column += 1 + rest.length() - stripped.length();
+                }
+                return hint(answer + ", or in column " + column + " to make it 
a key next to " + keyName(stripped)
+                            + " (line " + (i + 1) + ")");
+            }
+        }
+        return hint(answer);
+    }
+
     /**
      * "mapping values are not allowed here" has two causes: a key indented 
deeper than the keys around it, and a colon
      * inside a value that is not quoted. The parser points at the colon, so 
the key's own colon is the first.
@@ -918,8 +961,7 @@ public class YamlValidator {
                         .messageKey("type")
                         .format(new MessageFormat("{0}"))
                         .arguments(name + ": ${...} is simple syntax, not " + 
name + ": write the expression in " + name
-                                   + " (" + example + "), or use simple: 
{expression: \"" + text.replace("\"", "'")
-                                   + "\"}")
+                                   + " (" + example + "), or use simple: 
{expression: " + yamlQuoted(text) + "}")
                         .build());
             }
             if ("constant".equals(name) && !template) {
@@ -929,6 +971,14 @@ public class YamlValidator {
         }
     }
 
+    /**
+     * The text as a quoted YAML value, as written: in single quotes when it 
holds a double quote, as a JSON text does,
+     * so a hint does not turn {"error": ...} into {'error': ...}, which is 
not JSON.
+     */
+    static String yamlQuoted(String text) {
+        return text.contains("\"") || text.contains("\\") ? "'" + 
text.replace("'", "''") + "'" : "\"" + text + "\"";
+    }
+
     /** A simple function in a constant: ${header.sku}, ${body}, 
${exchangeProperty.x}, ${date:now:...}. */
     private static final Pattern SIMPLE_FUNCTION = 
Pattern.compile("\\$\\{[a-z][^}]*}");
 
@@ -954,7 +1004,7 @@ public class YamlValidator {
                     .messageKey("type")
                     .format(new MessageFormat("{0}"))
                     .arguments(m.group() + " is not evaluated, a constant is 
used as written: use simple:"
-                               + " {expression: \"" + text.replace("\"", "'") 
+ "\"} for a value with expressions")
+                               + " {expression: " + yamlQuoted(text) + "} for 
a value with expressions")
                     .build());
         }
     }
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/ConstantWithSimpleTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/ConstantWithSimpleTest.java
index bec32eec7133..e8061368dc38 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/ConstantWithSimpleTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/ConstantWithSimpleTest.java
@@ -54,7 +54,8 @@ public class ConstantWithSimpleTest {
         assertThat(lines(yaml)).singleElement().satisfies(m -> {
             assertThat(m).startsWith("Line 10: ");
             assertThat(m).contains("/constant: ${header.sku} is not evaluated, 
a constant is used as written");
-            assertThat(m).contains("use simple: {expression: \"{'error': 
'unknown sku ${header.sku}'}\"}");
+            // the JSON stays JSON: {'error': ...} is not JSON, and a model 
copies the hint as written
+            assertThat(m).contains("use simple: {expression: '{\"error\": 
\"unknown sku ${header.sku}\"}'}");
         });
     }
 
diff --git 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
index dbeb8e2d6a8a..0e8f8aaa265a 100644
--- 
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
+++ 
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/YamlValidatorPropertyHintTest.java
@@ -1283,4 +1283,98 @@ public class YamlValidatorPropertyHintTest {
                 .contains("the list that starts at line 5 has its items in 
column 9")
                 .doesNotContain("block sequence start");
     }
+
+    /** A language written as a step: no "did you mean 'sample'?", but where 
an expression goes. */
+    @Test
+    void aLanguageWrittenAsAStepSaysWhereItGoes() throws Exception {
+        List<Error> errors = validator.validate("""
+                - route:
+                    from:
+                      uri: direct:stock
+                      steps:
+                        - setBody:
+                            expression:
+                              jsonpath:
+                                expression: "$"
+                        - simple: "${body}"
+                        - log: "Stock service processing order"
+                """);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0).getMessage())
+                .contains("'simple' is a language, not a step")
+                .contains("- setBody: {expression: {simple: {expression: 
\"${body}\"}}}")
+                .doesNotContain("did you mean");
+    }
+
+    /** - stop as a step gets the raw "string found, object expected": a step 
is a key followed by its options. */
+    @Test
+    void aStepWrittenAsAPlainNameSaysToWriteItAsAKey() throws Exception {
+        List<Error> errors = validator.validate("""
+                - route:
+                    from:
+                      uri: direct:start
+                      steps:
+                        - filter:
+                            expression:
+                              simple:
+                                expression: "${body} == null"
+                            steps:
+                              - stop
+                        - to
+                        - halt
+                """);
+        assertThat(errors).extracting(Error::getMessage).containsExactly(
+                "a step is a key followed by its options: write - stop: {} 
(the options of the step, if any, go inside"
+                                                                         + " 
the braces)",
+                "a step is a key followed by its options: write - to: {uri: 
\"...\"}",
+                "a step is a key followed by its options, such as - log: 
{message: \"...\"} or - stop: {}; halt is not a"
+                                                                               
        + " step");
+    }
+
+    /**
+     * A key in the column of the dashes of a list gets "expected <block end>, 
but found '?'": steps: under the -
+     * simple: of a when:, meant for the item.
+     */
+    @Test
+    void aKeyInTheColumnOfTheDashesOfAListNamesBothColumns() throws Exception {
+        List<Error> errors = validator.validate("""
+                - route:
+                    from:
+                      uri: direct:start
+                      steps:
+                        - choice:
+                            when:
+                              - simple: "${body} == 'a'"
+                              steps:
+                                - log: "a"
+                """);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0).getMessage())
+                .isEqualTo("line 8: steps starts in column 15, the column of 
the - of the list item at line 7, so it is"
+                           + " part of neither that item nor the key that 
holds the list. Write it in column 17 to make"
+                           + " it a key of the item, as simple is, or in 
column 13 to make it a key next to when"
+                           + " (line 6)");
+    }
+
+    /** onFallback: under the - to: of the steps: of a circuitBreaker, meant 
for the circuit breaker. */
+    @Test
+    void aKeyInTheColumnOfTheDashesOfAListCanBelongToTheParent() throws 
Exception {
+        List<Error> errors = validator.validate("""
+                - route:
+                    from:
+                      uri: direct:start
+                      steps:
+                        - circuitBreaker:
+                            steps:
+                              - to:
+                                  uri: http://localhost:8080/x
+                              onFallback:
+                                steps:
+                                  - log: "fallback"
+                """);
+        assertThat(errors).hasSize(1);
+        assertThat(errors.get(0).getMessage())
+                .startsWith("line 9: onFallback starts in column 15, the 
column of the - of the list item at line 7")
+                .endsWith("as to is, or in column 13 to make it a key next to 
steps (line 6)");
+    }
 }

Reply via email to