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 c80af25f5569 chore: camel-jbang - the unknown-header check says what 
answers an invented Camel* header and what the trigger sets (#26569)
c80af25f5569 is described below

commit c80af25f5569b83f541fc643ada97414cace2696
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Sep 18 08:56:55 2026 +0200

    chore: camel-jbang - the unknown-header check says what answers an invented 
Camel* header and what the trigger sets (#26569)
    
    * chore: camel-jbang - the unknown-header check says what answers an 
invented Camel* header and what the trigger sets
    
    The check that flags a Camel* header no component in the file sets only 
went further than
    "the value would be null" when a real header was spelled similarly. In a 
timer-triggered
    route there is no message source to name, so a beginner or a model writes 
CamelFromEndpoint
    or CamelEndpointUri for "which endpoint did this come from", nothing is 
close, and the message
    gave nothing to write instead.
    
    The message now:
    - answers the intent behind the name when no real header is close: endpoint 
or route
      (${exchange.fromEndpoint}, ${routeId}, ${header.CamelToEndpoint}), size 
or count
      (${body.length()}, ${bodyAs(byte[]).length}, ${headers.size()}), time
      (${date:now:...}, ${date:exchangeCreated:...}, and when 
${messageTimestamp} is set);
    - always lists the headers of the first component in the file (the 
trigger), not only the
      owner of a did-you-mean match, and the timer's exchange properties 
whenever a timer is used;
    - ends with the rule that a header no component sets stays null unless a 
setHeader step
      sets it earlier in the route.
    
    Every expression named was checked against a running route. The check is 
shared by
    camel validate, the camel-jbang-mcp write and validate tools and the editor 
integrations.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Claude-Session: https://claude.ai/code/session_01Bp3538HRBPMQkb5ta9xRaj
    
    * chore: camel-jbang - drop the stale Javadoc left above 
HeaderChecks.EXCHANGE_PROPERTIES
    
    The block documented validateKnownHeaders before CAMEL-24715 moved the
    check into this class; the field below it has its own Javadoc.
    Pointed out in review.
    
    Co-Authored-By: Claude Opus 5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Fable 5.1 <[email protected]>
---
 .../dsl/jbang/core/commands/ai/HeaderChecks.java   | 60 +++++++++++++++++++---
 .../commands/ai/SourceValidatorEndpointTest.java   | 49 ++++++++++++++++++
 2 files changed, 102 insertions(+), 7 deletions(-)

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 0d520c946ee4..9a23ad0f6ffc 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
@@ -44,11 +44,6 @@ final class HeaderChecks {
 
     static final Pattern SCHEME_IN_URI_PATTERN = 
Pattern.compile("uri:\\s*\"?([a-zA-Z][a-zA-Z0-9+.-]*):");
 
-    /**
-     * A Camel* header that no component used in the file defines 
(CamelTimerIndex; the timer sets CamelTimerCounter):
-     * the value is null at runtime. Checked against the header metadata of 
every component the file names, with the
-     * closest real name.
-     */
     /**
      * Names a component sets as exchange properties, not headers; the catalog 
has no metadata for those, so the ones a
      * beginner reaches for are listed here (TimerConsumer sets them with 
setProperty).
@@ -56,6 +51,33 @@ final class HeaderChecks {
     static final Map<String, List<String>> EXCHANGE_PROPERTIES = Map.of(
             "timer", List.of("CamelTimerCounter", "CamelTimerName", 
"CamelTimerPeriod", "CamelTimerTime"));
 
+    /**
+     * 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
+     * a beginner or a model writes CamelFromEndpoint or CamelEndpointUri for 
"which endpoint did this come from" and
+     * was told only that the value would be null.
+     */
+    static final List<Map.Entry<Pattern, String>> INTENT_HINTS = List.of(
+            Map.entry(Pattern.compile("Endpoint|Uri|Source|From|Route|Origin"),
+                    "For the endpoint or route a message came from use 
${exchange.fromEndpoint} or ${routeId}; for the"
+                                                                               
 + " endpoint it was last sent to, ${header.CamelToEndpoint}."),
+            Map.entry(Pattern.compile("Size|Length|Count"),
+                    "The body size is ${body.length()} for a String body or 
${bodyAs(byte[]).length}; the number of"
+                                                            + " headers is 
${headers.size()}."),
+            Map.entry(Pattern.compile("Timestamp|Time|Date"),
+                    "The current time is ${date:now:yyyy-MM-dd HH:mm:ss} and 
the time the message was created is"
+                                                              + " 
${date:exchangeCreated:yyyy-MM-dd HH:mm:ss}; ${messageTimestamp} is set only by"
+                                                              + " components 
that stamp their messages."));
+
+    static String intentHint(String name) {
+        for (var e : INTENT_HINTS) {
+            if (e.getKey().matcher(name).find()) {
+                return e.getValue();
+            }
+        }
+        return null;
+    }
+
     public static List<String> validateKnownHeaders(String content, 
CamelCatalog catalog) {
         List<String> msgs = new ArrayList<>();
         if (content == null) {
@@ -130,15 +152,39 @@ final class HeaderChecks {
                         .append(" is not set by ").append(String.join(", ", 
schemes)).append(" (the value would be null)");
                 if (best != null) {
                     sb.append(": did you mean ").append(best).append("?");
+                } else {
+                    String hint = intentHint(name);
+                    if (hint != null) {
+                        sb.append(" ").append(hint);
+                    }
                 }
+                // what the file's components do set: the owner of the closest 
name, and the first component in the
+                // file (normally the trigger), so a name with nothing close 
still learns the real ones
+                Set<String> listed = new LinkedHashSet<>();
                 if (scheme != null) {
+                    listed.add(scheme);
+                }
+                listed.add(schemes.iterator().next());
+                for (String sc : listed) {
                     List<String> names = new ArrayList<>();
                     for (var e : owner.entrySet()) {
-                        if (e.getValue().equals(scheme)) {
+                        if (e.getValue().equals(sc)) {
                             names.add(e.getKey());
                         }
                     }
-                    sb.append(" The ").append(scheme).append(" headers are 
").append(String.join(", ", names)).append(".");
+                    if (!names.isEmpty()) {
+                        sb.append(" The ").append(sc).append(" headers are 
").append(String.join(", ", names)).append(".");
+                    }
+                }
+                for (String sc : schemes) {
+                    List<String> props = EXCHANGE_PROPERTIES.getOrDefault(sc, 
List.of());
+                    if (!props.isEmpty()) {
+                        sb.append(" ").append(sc).append(" sets the exchange 
properties ").append(String.join(", ", props))
+                                .append(" 
(${exchangeProperty.").append(props.get(0)).append("}).");
+                    }
+                }
+                if (m.group(1) == null) {
+                    sb.append(" A header no component sets stays null unless a 
setHeader step sets it earlier in the route.");
                 }
                 msgs.add(sb.toString());
             }
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 a9694ee8908b..1005a0c05bc7 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
@@ -398,6 +398,55 @@ class SourceValidatorEndpointTest {
                 .contains("${exchangeProperty.CamelTimerCounter}");
     }
 
+    @Test
+    void anInventedHeaderForTheSourceEndpointGetsTheExpressionThatAnswersIt() {
+        // a timer-triggered route has no message source to name, so a 
beginner or a model invents CamelFromEndpoint for
+        // "which endpoint did this come from"; nothing is close, so the 
message must say what does answer it and what
+        // the timer sets
+        List<String> msgs = SourceValidator.validateKnownHeaders("""
+                - from:
+                    uri: "timer:tick?period=1000"
+                    steps:
+                      - log: "Endpoint: ${header.CamelFromEndpoint} size: 
${body.length()}"
+                """, catalog);
+        assertThat(msgs).hasSize(1);
+        assertThat(msgs.get(0)).contains("header CamelFromEndpoint is not set 
by timer").doesNotContain("did you mean")
+                
.contains("${exchange.fromEndpoint}").contains("${routeId}").contains("${header.CamelToEndpoint}")
+                .contains("The timer headers are CamelTimerFiredTime")
+                .contains(
+                        "timer sets the exchange properties CamelTimerCounter, 
CamelTimerName, CamelTimerPeriod, CamelTimerTime")
+                .contains("${exchangeProperty.CamelTimerCounter}")
+                .contains("stays null unless a setHeader step sets it earlier 
in the route");
+    }
+
+    @Test
+    void anInventedHeaderWithTwoComponentsListsTheTriggersHeaders() {
+        // the first component in the file is the trigger; its headers are 
listed even when nothing is close
+        List<String> msgs = SourceValidator.validateKnownHeaders("""
+                - from:
+                    uri: "file:in?noop=true"
+                    steps:
+                      - log: "${header.CamelEndpointUri} 
${header.CamelFileName}"
+                      - to:
+                          uri: "timer:ignored"
+                """, catalog);
+        assertThat(msgs).hasSize(1);
+        assertThat(msgs.get(0)).contains("header CamelEndpointUri is not set 
by file, timer")
+                .contains("${exchange.fromEndpoint}").contains("The file 
headers are").contains("CamelFileName");
+    }
+
+    @Test
+    void anInventedSizeHeaderNamesTheBodyFunctions() {
+        List<String> msgs = SourceValidator.validateKnownHeaders("""
+                - from:
+                    uri: "timer:tick?period=1000"
+                    steps:
+                      - log: "${header.CamelPayloadSize}"
+                """, catalog);
+        assertThat(msgs).hasSize(1);
+        
assertThat(msgs.get(0)).contains("${body.length()}").contains("${headers.size()}");
+    }
+
     @Test
     void aPropertyOrBeanNameIsNotAHeader() {
         // CAMEL-24710: name: counts as a header only under 
setHeader/removeHeader; a dotted name is looked up as is,

Reply via email to