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