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

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

commit 35c639870256a0f1679d08c92d99873c3317656b
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 22 08:09:25 2026 +0200

    CAMEL-25952: camel-jbang - AI tools: say where a language option such as 
resultType goes, and note a JMS reply header set by hand
    
    From the local-model benchmark of Oct 21-22 2026:
    
    - the YAML validator answers an option of a language written next to its 
short form
      (expression: {jsonpath: "...", resultType: java.util.Map}) with where it 
goes, for any language and option
    - a route that sets or reads a JMS reply header by hand (JMSReplyTo, 
replyTo, camelJmsReplyTo), with nothing sent
      InOut, gets a note in the write and validate answers, and the write 
message points to it
    - the header checks also run for an endpoint written as the scheme alone 
(uri: jms, with parameters:)
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
---
 .../dsl/jbang/core/commands/ai/AuthoringTools.java | 19 ++++++-
 .../dsl/jbang/core/commands/ai/HeaderChecks.java   | 42 +++++++++++++-
 .../core/commands/ai/AuthoringToolsEditTest.java   | 26 +++++++++
 .../commands/ai/SourceValidatorEndpointTest.java   | 65 ++++++++++++++++++++++
 .../camel/dsl/yaml/validator/SchemaHints.java      | 27 +++++++++
 .../camel/dsl/yaml/validator/YamlValidator.java    | 28 ++++++++++
 .../validator/YamlValidatorPropertyHintTest.java   | 37 ++++++++++++
 7 files changed, 241 insertions(+), 3 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 31174238a18a..ae991eab9d8d 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -1412,8 +1412,9 @@ public final class AuthoringTools {
     }
 
     /**
-     * What a Kamelet file does that works but is not right, which does not 
refuse a write: a camel: dependency its
-     * template does not use (CAMEL-25403).
+     * What a file does that works but is not right, which does not refuse a 
write: a camel: dependency a Kamelet's
+     * template does not use (CAMEL-25403), a {expression} in the text of an 
XSLT element, a JMS reply header set by
+     * hand in a route that sends nothing InOut.
      */
     private static void putFileNotes(JsonObject result, String file, String 
content) {
         String name = file != null ? file.toLowerCase(Locale.ROOT) : "";
@@ -1428,6 +1429,12 @@ public final class AuthoringTools {
             if (!notes.isEmpty()) {
                 result.put("notes", new JsonArray(notes));
             }
+        } else if (name.endsWith(".yaml") || name.endsWith(".yml")) {
+            // a JMS reply header set by hand, with nothing sent InOut
+            List<String> notes = HeaderChecks.replyHeaderNotes(content);
+            if (!notes.isEmpty()) {
+                result.put("notes", new JsonArray(notes));
+            }
         }
     }
 
@@ -1678,8 +1685,12 @@ public final class AuthoringTools {
             result.put("existingProblems", new JsonArray(problemsBefore));
         }
         UnconsumedDirect waiting = null;
+        boolean replyByHand = false;
         if (validate) {
             putFileNotes(result, file, content);
+            String lower = file.toLowerCase(Locale.ROOT);
+            replyByHand = (lower.endsWith(".yaml") || lower.endsWith(".yml"))
+                    && !HeaderChecks.replyHeaderNotes(content).isEmpty();
             waiting = unconsumedDirect(dir, file, content, ctx.catalog());
             if (waiting != null) {
                 JsonArray notes = result.get("notes") instanceof JsonArray 
existing ? existing : new JsonArray();
@@ -1729,6 +1740,10 @@ public final class AuthoringTools {
         if (waiting != null) {
             result.put("message", result.getString("message") + " Note: " + 
waiting.summary() + " (see notes).");
         }
+        if (replyByHand) {
+            result.put("message", result.getString("message") + " Note: a JMS 
reply header is set by hand, and nothing"
+                                  + " is sent with pattern: InOut (see 
notes).");
+        }
         if (!repaired.isEmpty()) {
             result.put("repaired", new JsonArray(repaired));
             result.put("message", "Written with these repairs of the YAML (the 
file has them now): "
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
index e8294076ad79..e75701a2e1d3 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/HeaderChecks.java
@@ -56,7 +56,9 @@ final class HeaderChecks {
      */
     static final Pattern EXCHANGE_CONSTANT_NAME = 
Pattern.compile("name:\\s*['\"]?Exchange\\.([A-Z][A-Z0-9_]*)");
 
-    static final Pattern SCHEME_IN_URI_PATTERN = 
Pattern.compile("uri:\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*):");
+    // the scheme of a URI (uri: "jms:queue:x"), or the scheme alone (uri: 
jms, with the rest under parameters:)
+    static final Pattern SCHEME_IN_URI_PATTERN
+            = 
Pattern.compile("(?m)uri:\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*)(?::|\"?\\s*$)");
 
     /**
      * Names a component sets as exchange properties, not headers; the catalog 
has no metadata for those, so the ones a
@@ -115,6 +117,44 @@ final class HeaderChecks {
               + " reply (the body at the end of the service route); you do not 
need to set JMSReplyTo or replyTo"
               + " yourself.";
 
+    /**
+     * A reply header of a JMS request-reply named by hand, in any case: 
JMSReplyTo, replyTo, jmsReplyTo,
+     * camelJmsReplyTo. The names that start with Camel and a capital are left 
to the header check, which reports them.
+     */
+    private static final Pattern REPLY_HEADER_REF = Pattern.compile(
+            
"(?:\\$\\{headers?\\.|headers\\.|headers\\[['\"]|header\\(['\"]|(name):\\s*['\"]?)"
+                                                                    + 
"((?i:(?:camel)?(?:jms)?replyto))(?![A-Za-z0-9])");
+
+    /** An endpoint of a JMS component, in a URI (jms:queue:x) or as the 
scheme alone (uri: jms, with parameters). */
+    private static final Pattern JMS_URI = Pattern.compile(
+            
"(?m)uri:\\s*['\"]?(?:jms|sjms2?|activemq6?|amqp)(?::|['\"]?\\s*$)");
+
+    /**
+     * Notes for a route that sets or reads a JMS reply header by hand when 
nothing is sent with InOut (r21 of the
+     * local-model benchmark: a model set JMSReplyTo in the service route and 
sent the request one way). A note, not an
+     * error: a reply queue set by hand for a reply that another route 
consumes is a design of its own.
+     */
+    static List<String> replyHeaderNotes(String content) {
+        List<String> notes = new ArrayList<>();
+        if (content == null || !JMS_URI.matcher(content).find() || 
content.contains("InOut")) {
+            return notes;
+        }
+        String[] lines = content.split("\n", -1);
+        Set<String> seen = new HashSet<>();
+        for (int i = 0; i < lines.length; i++) {
+            Matcher m = REPLY_HEADER_REF.matcher(lines[i]);
+            while (m.find()) {
+                String name = m.group(2);
+                if (name.startsWith("Camel") || (m.group(1) != null && 
!isHeaderName(lines, i)) || !seen.add(name)) {
+                    continue;
+                }
+                notes.add("Line " + (i + 1) + ": " + name + (m.group(1) != 
null ? " is set by hand" : " is read")
+                          + ", and no message is sent with pattern: InOut. " + 
JMS_REQUEST_REPLY_HINT);
+            }
+        }
+        return notes;
+    }
+
     /**
      * What an invented Camel* header is usually reaching for, keyed on a 
fragment of its name, and the expression that
      * answers it. Tried when no real header is close: in a timer-triggered 
route there is no message source to name, so
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
index 2784e3791b58..8d60d33ac71c 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringToolsEditTest.java
@@ -336,6 +336,32 @@ class AuthoringToolsEditTest {
         assertThat(whole.getString("message")).doesNotContain("Note:");
     }
 
+    /** A JMS reply header set by hand, with nothing sent InOut: the write 
goes through, with a note in the answer. */
+    @Test
+    void aJmsReplyHeaderSetByHandIsANoteOfTheWrite(@TempDir Path dir) {
+        String content = """
+                - route:
+                    from:
+                      uri: jms:queue:stock
+                      steps:
+                        - setHeader:
+                            name: JMSReplyTo
+                            constant: orders
+                        - setBody:
+                            constant: reserved
+                """;
+
+        JsonObject result = AuthoringTools.writeFile(new ToolContext(), dir, 
"stock.camel.yaml", content, true);
+
+        
assertThat(result.getString("status")).as(result.toJson()).isNotEqualTo("invalid");
+        JsonArray notes = (JsonArray) result.get("notes");
+        assertThat(notes).hasSize(1);
+        assertThat(notes.get(0).toString()).startsWith("Line 6: JMSReplyTo is 
set by hand")
+                .contains("send with pattern: InOut on the to:");
+        assertThat(result.getString("message"))
+                .endsWith("Note: a JMS reply header is set by hand, and 
nothing is sent with pattern: InOut (see notes).");
+    }
+
     private static JsonObject change(String find, String replace) {
         JsonObject jo = new JsonObject();
         jo.put("find", find);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
index d3325f4a0527..bd4c6066fd49 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/SourceValidatorEndpointTest.java
@@ -607,6 +607,71 @@ class SourceValidatorEndpointTest {
                 .doesNotContain("did you mean"));
     }
 
+    /**
+     * A reply header set by hand in a route that sends nothing InOut gets a 
note, in any case of its name (r21 of the
+     * local-model benchmark: the service route set JMSReplyTo and the request 
went one way). The endpoints are written
+     * as the scheme alone with parameters, as well as in a URI.
+     */
+    @Test
+    void aJmsReplyHeaderSetByHandGetsANote() {
+        String route = """
+                - route:
+                    id: reserve-order
+                    from:
+                      uri: jms
+                      parameters:
+                        destinationType: queue
+                        destinationName: orders
+                      steps:
+                        - toD:
+                            uri: "jms:queue:stock"
+                        - log:
+                            message: "Stock service answered: 
${header.camelJmsReplyTo}"
+                - route:
+                    id: stock-service
+                    from:
+                      uri: jms
+                      parameters:
+                        destinationName: stock
+                      steps:
+                        - setHeader:
+                            name: JMSReplyTo
+                            expression:
+                              simple: "jms:queue:orders"
+                        - setBody:
+                            constant: reserved
+                """;
+        assertThat(HeaderChecks.replyHeaderNotes(route)).containsExactly(
+                "Line 12: camelJmsReplyTo is read, and no message is sent with 
pattern: InOut. "
+                                                                         + 
HeaderChecks.JMS_REQUEST_REPLY_HINT,
+                "Line 21: JMSReplyTo is set by hand, and no message is sent 
with pattern: InOut. "
+                                                                               
                                 + HeaderChecks.JMS_REQUEST_REPLY_HINT);
+        // with the request sent InOut, the reply is Camel's: no note
+        assertThat(HeaderChecks.replyHeaderNotes(route.replace("uri: 
\"jms:queue:stock\"",
+                "uri: \"jms:queue:stock\"\n            pattern: 
InOut"))).isEmpty();
+        // not a JMS route: no note
+        assertThat(HeaderChecks.replyHeaderNotes(route.replace("uri: jms", 
"uri: seda").replace("jms:queue:", "seda:")))
+                .isEmpty();
+    }
+
+    /** The headers of an endpoint written as the scheme alone (uri: jms, with 
parameters:) are checked too. */
+    @Test
+    void theHeadersOfAnEndpointWrittenAsTheSchemeAloneAreChecked() {
+        List<String> msgs = SourceValidator.validateKnownHeaders("""
+                - route:
+                    from:
+                      uri: jms
+                      parameters:
+                        destinationName: orders
+                      steps:
+                        - log:
+                            message: "${header.CamelJmsDestinationNam}"
+                """, catalog);
+        assertThat(msgs).singleElement().satisfies(m -> assertThat(m)
+                .contains("header CamelJmsDestinationNam is not set by jms")
+                .contains("did you mean CamelJmsDestination, 
CamelJmsDestinationName?"));
+    }
+
     /** The JMS request-reply hint is for JMS only: the reply headers of other 
components keep their "did you mean". */
     @Test
     void aReplyHeaderOfAnotherComponentGetsDidYouMean() {
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 91e852f297a8..031038a77842 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
@@ -156,6 +156,23 @@ final class SchemaHints {
         return value != null && value.isValueNode() && !value.isNull() ? 
value.asText() : fallback;
     }
 
+    /**
+     * The language of the error's node that is written in its short form 
(jsonpath: "$[...]") next to the unknown
+     * property, when that property is one of the language's own options 
(resultType), or null.
+     */
+    private static String shortFormLanguage(Match m) {
+        JsonNode instance = m.error().getInstanceNode();
+        if (instance == null || !instance.isObject() || 
instance.has("expression")) {
+            return null;
+        }
+        for (String language : m.validator().languageKeys()) {
+            if (instance.path(language).isTextual() && 
m.validator().languageOptions(language).contains(m.unknown())) {
+                return language;
+            }
+        }
+        return null;
+    }
+
     /** 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",
@@ -622,6 +639,16 @@ final class SchemaHints {
             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})"),
+            // expression: {jsonpath: "$[...]", resultType: java.util.Map}: an 
option of the language written next to its
+            // short form, where the node has no such property; it goes under 
the language, with the text as expression:
+            unknownProperty(null, m -> shortFormLanguage(m) != null,
+                    m -> {
+                        String language = shortFormLanguage(m);
+                        return m.unknown() + " is an option of the " + 
language + " language: write the language as a"
+                               + " map, with the text under expression: and " 
+ m.unknown() + " next to it (" + language
+                               + ": {expression: " + 
YamlValidator.yamlQuoted(valueOf(m, language, "...")) + ", "
+                               + m.unknown() + ": " + valueOf(m, m.unknown(), 
"...") + "})";
+                    }),
             // onException: {maximumRedeliveries: 3}, deadLetterChannel: 
{retryAttemptedLogLevel: WARN}: an option of
             // the redelivery policy written next to it, where the closest 
name (level) is not what was meant
             unknownProperty(".*/(onException|" + String.join("|", 
ROUTE_ERROR_HANDLER_KINDS) + ")",
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 d51ac3e72cb8..dcb624767cf0 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
@@ -873,6 +873,34 @@ public class YamlValidator {
         return property + ": \"...\"";
     }
 
+    /** The options of a language (resultType, suppressExceptions...) as the 
schema lists them, or empty. */
+    Set<String> languageOptions(String language) {
+        // the language's definition is referenced from an anyOf entry of 
ExpressionDefinition (the entry under its
+        // properties can be an empty placeholder)
+        JsonNode ref = null;
+        for (JsonNode value : 
model.at("/items/definitions/org.apache.camel.model.language.ExpressionDefinition")
+                .findValues(language)) {
+            if (value.path("$ref").isTextual()) {
+                ref = value.path("$ref");
+                break;
+            }
+        }
+        JsonNode definition = ref != null ? 
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 the short form (a 
string) 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);
+        }
+        answer.remove("expression");
+        return answer;
+    }
+
     /**
      * The EIPs whose expression the runtime needs: the schema leaves it 
optional for every expression node, and a split
      * written with only delimiter: "," (or a filter, setBody, when... with 
only options) fails when the route is
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 0e8f8aaa265a..68807cc68354 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
@@ -167,6 +167,43 @@ public class YamlValidatorPropertyHintTest {
         }
     }
 
+    /**
+     * An option of a language written next to its short form (a local model 
in the benchmark of Oct 21-22 2026 wrote
+     * resultType there, was told only that the property is not defined, and 
wrote the same file 23 times).
+     */
+    @Test
+    public void testAnOptionNextToTheShortFormOfALanguageSaysWhereItGoes() 
throws Exception {
+        for (YamlValidator v : bothModes()) {
+            List<Error> errors = v.validate("""
+                    - route:
+                        from:
+                          uri: direct:get-order
+                          steps:
+                            - setBody:
+                                expression:
+                                  jsonpath: "$[?(@.orderId == '${header.id}')]"
+                                  resultType: java.util.Map
+                    """);
+            assertThat(errors).extracting(Error::getMessage)
+                    .anyMatch(m -> m.contains("resultType is an option of the 
jsonpath language: write the language as a"
+                                              + " map, with the text under 
expression: and resultType next to it"
+                                              + " (jsonpath: {expression: 
\"$[?(@.orderId == '${header.id}')]\","
+                                              + " resultType: 
java.util.Map})"));
+        }
+        // the full form is valid
+        assertThat(validator.validate("""
+                - route:
+                    from:
+                      uri: direct:get-order
+                      steps:
+                        - setBody:
+                            expression:
+                              jsonpath:
+                                expression: "$[?(@.orderId == '${header.id}')]"
+                                resultType: java.util.Map
+                """)).isEmpty();
+    }
+
     /** 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 {

Reply via email to