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