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,