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 29fac56707ec CAMEL-24888: the validator says the shape for the seven 
mistakes of the HTTP rungs
29fac56707ec is described below

commit 29fac56707ec2c466a29bcab1152622dad3bffb5
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Sep 22 12:05:07 2026 +0200

    CAMEL-24888: the validator says the shape for the seven mistakes of the 
HTTP rungs
    
    On the HTTP rungs of the examples ladder (rest, http, rest-openapi) the 
local
    model failed 22 of 32 steps in the camel-jbang-mcp stepwise benchmark, and 
its
    refused writes cluster on seven shapes the validator did not name a fix for.
    Each now gets a hint that states the form to write:
    
    - constant: null to clear the body -> setBody with simple ${null}
    - setExchangeProperty (and siblings) -> the EIP is setProperty
    - a double-quoted value never closed -> "line N: the value opens a double
      quote and never closes it", from the Jackson and snakeyaml message alike
    - toD: {options: ...} -> options go under parameters: like to:
    - library: jackson -> the library name is case sensitive, taken from the
      schema's own enum list so each data format names its own entries
    - jsonpath: {jsonPath: ...} -> the text goes under expression:
    - rest-openapi path/pathParameters/queryParameters option -> the parameter
      comes from a header of the same name
    
    Covered by YamlValidatorPropertyHintTest (both schema modes) and
    SourceValidatorPlaceholderTest.
    
    Closes #26732
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
---
 .../dsl/jbang/core/commands/ai/EndpointChecks.java | 10 +++
 .../ai/SourceValidatorPlaceholderTest.java         | 27 ++++++++
 .../camel/dsl/yaml/validator/SchemaHints.java      | 38 +++++++++++
 .../camel/dsl/yaml/validator/YamlValidator.java    | 30 +++++++++
 .../validator/YamlValidatorPropertyHintTest.java   | 77 ++++++++++++++++++++++
 5 files changed, 182 insertions(+)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
index 1a24f34daa9f..0163b9bb44a4 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/EndpointChecks.java
@@ -282,6 +282,16 @@ final class EndpointChecks {
 
     /** Options models write that the component does not have, and what the 
component does instead. */
     static final Map<String, String> INVENTED_OPTIONS = Map.ofEntries(
+            // CAMEL-24888: the path parameters of an OpenAPI operation are 
headers of the same name
+            Map.entry("rest-openapi:path",
+                    "a path parameter of the operation, {sku} in /stock/{sku}, 
comes from a header of the same name: add"
+                                           + " setHeader: {name: sku, ...} 
before the call, the operation's path is in the contract"),
+            Map.entry("rest-openapi:pathParameters",
+                    "a path parameter of the operation comes from a header of 
the same name: add setHeader: {name: sku,"
+                                                     + " ...} before the 
call"),
+            Map.entry("rest-openapi:queryParameters",
+                    "a query parameter of the operation comes from a header of 
the same name: add setHeader: {name: page,"
+                                                      + " ...} before the 
call"),
             Map.entry("file:mkdir", "directories are created by default 
(autoCreate=true); remove the option"),
             Map.entry("file:createDirectory", "directories are created by 
default (autoCreate=true); remove the option"),
             Map.entry("file:overwrite", "an existing file is overridden by 
default (fileExist=Override); remove the option"),
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPlaceholderTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPlaceholderTest.java
index 36822c18c4fd..4019b2a2dcfe 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPlaceholderTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorPlaceholderTest.java
@@ -32,6 +32,33 @@ class SourceValidatorPlaceholderTest {
 
     private static final CamelCatalog catalog = new DefaultCamelCatalog();
 
+    /** CAMEL-24888: a path parameter of an OpenAPI operation is a header, not 
an endpoint option. */
+    @Test
+    void aRestOpenApiPathOptionSaysTheParameterIsAHeader() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: timer:tick
+                      steps:
+                        - to:
+                            uri: rest-openapi
+                            parameters:
+                              specificationUri: stock-api.json
+                              operationId: reserveStock
+                              path: "sku=${exchangeProperty.sku}"
+                """;
+        List<String> errors = SourceValidator.validateYamlEndpoints(yaml, 
catalog);
+        assertThat(errors).anyMatch(e -> e.contains("rest-openapi: Unknown 
option 'path'")
+                && e.contains("comes from a header of the same name: add 
setHeader: {name: sku"));
+        // the sibling spellings say the same
+        for (String option : List.of("pathParameters", "queryParameters")) {
+            List<String> more
+                    = 
SourceValidator.validateYamlEndpoints(yaml.replace("path: \"sku=", option + ": 
\"sku="), catalog);
+            assertThat(more).anyMatch(e -> e.contains("rest-openapi: Unknown 
option '" + option + "'")
+                    && e.contains("comes from a header of the same name: add 
setHeader: {name: "));
+        }
+    }
+
     @Test
     void aSimplePlaceholderInAnEndpointOptionSaysToWriteAPropertyPlaceholder() 
{
         String yaml = """
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 9f9acbc4b47f..a8b2e96b09fe 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
@@ -116,6 +116,12 @@ final class SchemaHints {
 
     private static final Predicate<Match> ANY = m -> true;
 
+    /** The EIP names a model writes for the exchange properties, and the ones 
Camel has. */
+    private static final Map<String, String> EXCHANGE_PROPERTY_EIPS = Map.of(
+            "setExchangeProperty", "setProperty", "setExchangeProperties", 
"setProperties",
+            "removeExchangeProperty", "removeProperty", 
"removeExchangeProperties", "removeProperties",
+            "setExchangeVariable", "setVariable", "setExchangeVariables", 
"setVariables");
+
     private static final Set<String> ROUTE_ERROR_HANDLER_KINDS
             = Set.of("noErrorHandler", "deadLetterChannel", 
"defaultErrorHandler", "springTransactionErrorHandler",
                     "jtaTransactionErrorHandler", "refErrorHandler");
@@ -230,6 +236,27 @@ final class SchemaHints {
                         return eip + " holds its EIPs under steps: " + eip + 
": {steps: [- log: \"...\"]}"
                                + (eip.equals("doCatch") ? ", each - doCatch: 
item with exception: and steps:" : "");
                     }),
+            // setBody: {constant: null} to clear the body before a GET: 
constant is a text (CAMEL-24888)
+            append("type", ".*/constant(/expression)?", m -> 
m.message().contains("null found"),
+                    m -> "constant is a text; to set an empty body (a GET 
sends none) write setBody: {simple:"
+                         + " {expression: \"${null}\"}}"),
+            // library: jackson: the enumeration is case sensitive; the name 
to write is the entry of this data
+            // format's enumeration (json, avro, protobuf, yaml each have 
their own) that matches ignoring case
+            append("enum", ".*/library", ANY,
+                    m -> {
+                        JsonNode instance = m.error().getInstanceNode();
+                        String written = instance != null && 
instance.isValueNode() ? instance.asText() : "";
+                        String list = between(m.message(), "[", "]");
+                        String match = null;
+                        for (String entry : (list == null ? "" : 
list).split(",")) {
+                            String name = entry.trim().replace("\"", "");
+                            if (!name.isEmpty() && 
name.equalsIgnoreCase(written)) {
+                                match = name;
+                            }
+                        }
+                        return "the library name is case sensitive"
+                               + (match != null ? ": write library: " + match 
: ", write it as listed");
+                    }),
             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:"),
@@ -384,6 +411,17 @@ final class SchemaHints {
                     m -> "step is the Step EIP, a named group: its EIPs go in 
its steps: list (step: {id: ..., steps: [-"
                          + " setHeader: ...]})",
                     "additionalProperties", "additionalProperties"),
+            // CAMEL-24888 (the HTTP rungs of the examples ladder): the shapes 
a model writes for the EIPs of a REST app
+            unknownProperty(null, m -> 
EXCHANGE_PROPERTY_EIPS.containsKey(m.unknown()),
+                    m -> "the EIP is " + 
EXCHANGE_PROPERTY_EIPS.get(m.unknown()) + ": write - "
+                         + EXCHANGE_PROPERTY_EIPS.get(m.unknown()) + ": {name: 
..., expression: {simple: {expression:"
+                         + " \"...\"}}} (an exchange property is read back as 
${exchangeProperty.name})"),
+            unknownProperty(".*/toD", m -> m.unknown().equals("options") || 
m.unknown().equals("params"),
+                    m -> "toD takes its options like to: under parameters: 
(toD: {uri: \"http://...\";, parameters:"
+                         + " {throwExceptionOnFailure: false}}), or in the uri 
after ?"),
+            unknownProperty(".*/jsonpath", m -> 
m.unknown().equalsIgnoreCase("jsonPath") || m.unknown().equals("path"),
+                    m -> "the JSONPath text goes under expression: (jsonpath: 
{expression: \"$[?(@.sku == 'X')]\","
+                         + " resultType: java.util.List})"),
             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 128dc98b56ef..2b47f748c2dc 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
@@ -206,6 +206,14 @@ public class YamlValidator {
                             .build();
                 }
             }
+            // a double-quoted value that never closes: the parser swallows 
the next lines and gives up further down
+            // (CAMEL-24888); the line that opened the quote is within the few 
lines above the reported one
+            for (int l = last; l >= 1 && l > last - 8; l--) {
+                Error unclosed = unclosedQuote(new Mark(l, 1), lines);
+                if (unclosed != null) {
+                    return unclosed;
+                }
+            }
             Error marked = indentationError(msg, lines);
             return marked != null ? marked : plain;
         }
@@ -253,6 +261,28 @@ public class YamlValidator {
         if (msg.contains("found unknown escape character")) {
             return unknownEscape(msg, problem, lines);
         }
+        if (msg.contains("expected <block end>, but found '<scalar>'") && 
!marks.isEmpty()) {
+            return unclosedQuote(marks.get(0), lines);
+        }
+        return null;
+    }
+
+    /**
+     * CAMEL-24888: {@code expression: "$[?(@.sku == '${header.sku}')]} with 
no closing quote: the parser swallows the
+     * following lines into the value and gives up at the next key. Says which 
line opened the quote.
+     */
+    static Error unclosedQuote(Mark start, String[] lines) {
+        if (start.line() < 1 || start.line() > lines.length) {
+            return null;
+        }
+        String line = lines[start.line() - 1];
+        int colon = line.indexOf(':');
+        String value = colon >= 0 ? line.substring(colon + 1).trim() : 
line.trim();
+        long quotes = value.chars().filter(c -> c == '"').count() - 
value.split("\\\\\"", -1).length + 1;
+        if (value.startsWith("\"") && quotes % 2 == 1) {
+            return hint("line " + start.line() + ": the value opens a double 
quote and never closes it: end it with"
+                        + " a \" after the last character (" + value + "\")");
+        }
         return null;
     }
 
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 a5d9b671afa6..03d33edc27a6 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
@@ -90,6 +90,83 @@ public class YamlValidatorPropertyHintTest {
         }
     }
 
+    /** CAMEL-24888: the shapes the local model wrote on the HTTP rungs, each 
with the form to write. */
+    @Test
+    public void testHttpRungShapesGetTheForm() throws Exception {
+        for (YamlValidator v : bothModes()) {
+            List<Error> errors = v.validate("""
+                    - route:
+                        from:
+                          uri: file:orders
+                          steps:
+                            - setExchangeProperty:
+                                name: orderId
+                                expression:
+                                  simple:
+                                    expression: "${body[orderId]}"
+                            - setBody:
+                                expression:
+                                  constant: null
+                            - marshal:
+                                json:
+                                  library: jackson
+                    """);
+            assertThat(errors).extracting(Error::getMessage)
+                    .anyMatch(m -> m.contains("setExchangeProperty") && 
m.contains("the EIP is setProperty"))
+                    .anyMatch(m -> m.contains("constant is a text") && 
m.contains("simple: {expression: \"${null}\"}"))
+                    .anyMatch(m -> m.contains("the library name is case 
sensitive: write library: Jackson"));
+            // the name comes from the data format's own enumeration, not from 
json's
+            errors = v.validate("""
+                    - route:
+                        from:
+                          uri: file:orders
+                          steps:
+                            - marshal:
+                                avro:
+                                  library: apacheavro
+                    """);
+            assertThat(errors).extracting(Error::getMessage)
+                    .anyMatch(m -> m.contains("the library name is case 
sensitive: write library: ApacheAvro")
+                            && !m.contains("Gson"));
+            errors = v.validate("""
+                    - route:
+                        from:
+                          uri: file:orders
+                          steps:
+                            - toD:
+                                uri: 
"http://localhost:8080/stock/${exchangeProperty.sku}";
+                                options:
+                                  throwExceptionOnFailure: false
+                            - setBody:
+                                expression:
+                                  jsonpath:
+                                    jsonPath: "$[?(@.sku == 'X')]"
+                    """);
+            assertThat(errors).extracting(Error::getMessage)
+                    .anyMatch(m -> m.contains("toD takes its options like to: 
under parameters:"))
+                    .anyMatch(m -> m.contains("the JSONPath text goes under 
expression:"));
+        }
+    }
+
+    /** CAMEL-24888: a double-quoted value that is never closed is named, 
instead of the parser's block-end message. */
+    @Test
+    public void testAnUnclosedQuoteIsNamed() throws Exception {
+        List<Error> errors = validator.validate("""
+                - route:
+                    from:
+                      uri: direct:a
+                      steps:
+                        - setBody:
+                            expression:
+                              jsonpath:
+                                expression: "$[?(@.sku == '${header.sku}')]
+                                resultType: java.util.List
+                        - log: "done"
+                """);
+        assertThat(errors).extracting(Error::getMessage)
+                .anyMatch(m -> m.startsWith("line 8: the value opens a double 
quote and never closes it"));
+    }
+
     @Test
     public void testPollEnrichWithAUriSaysItIsAnExpression() throws Exception {
         for (YamlValidator v : bothModes()) {

Reply via email to