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 eb8c99f15e74 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 (#28151)
eb8c99f15e74 is described below
commit eb8c99f15e740c9211845e569ab67061ffb28aaf
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Oct 20 12:41:01 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 (#28151)
Four validator messages did not help a coding agent fix the route:
- A language written as a step (- simple: "${body}") now 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) now 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) now 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.
Closes #28151
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../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)");
+ }
}