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 f4d7fae9f8bf CAMEL-25952: camel-jbang - AI tools: say where a language
option such as resultType goes, and note a JMS reply header set by hand (#28269)
f4d7fae9f8bf is described below
commit f4d7fae9f8bffdb83b9d8ce18979ed2673aa6da2
Author: Claus Ibsen <[email protected]>
AuthorDate: Sun Oct 25 12:58:27 2026 +0100
CAMEL-25952: camel-jbang - AI tools: say where a language option such as
resultType goes, and note a JMS reply header set by hand (#28269)
The YAML validator now says where a language option written next to the
short form of a language goes (write the language as a map, with the text under
expression: and the option next to it), instead of only that the property is
not in the schema. A route that sets or reads a JMS reply header by hand, where
nothing is sent with InOut, gets a note in the answers of camel_write_file and
camel_validate_source. The header checks also run for an endpoint written as
the scheme alone with it [...]
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
---
.../dsl/jbang/core/commands/ai/AuthoringTools.java | 19 ++++++-
.../dsl/jbang/core/commands/ai/HeaderChecks.java | 47 +++++++++++++++-
.../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, 244 insertions(+), 5 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 043f1e2f196a..ba7b23274be5 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
@@ -1477,8 +1477,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) : "";
@@ -1493,6 +1494,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));
+ }
}
}
@@ -1756,8 +1763,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();
@@ -1807,6 +1818,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 9106cc393362..477e61ddba99 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
@@ -59,11 +59,14 @@ final class HeaderChecks {
/**
* The scheme of an endpoint: of a uri: (from, to, wireTap...), and of the
steps the YAML DSL takes in the short
* form, such as to: aws-secrets-manager:sm (CAMEL-26039). The value is on
the line of the key, so the long form of
- * a to: with its uri: on the next line does not name a key such as uri:
or steps: as a scheme.
+ * a to: with its uri: on the next line does not name a key such as uri:
or steps: as a scheme. A uri: can also hold
+ * the scheme alone (uri: jms, with the rest under parameters:).
*/
static final Pattern SCHEME_IN_URI_PATTERN = Pattern.compile(
-
"(?<![A-Za-z0-9_-])(?:uri|to|toD|to-d|poll|interceptFrom|intercept-from|interceptSendToEndpoint"
- +
"|intercept-send-to-endpoint):[ \\t]*['\"]?([a-zA-Z][a-zA-Z0-9+.-]*):");
+
"(?m)(?<![A-Za-z0-9_-])(?:(?:uri|to|toD|to-d|poll|interceptFrom|intercept-from|interceptSendToEndpoint"
+ +
"|intercept-send-to-endpoint):[ \\t]*['\"]?(?=[a-zA-Z][a-zA-Z0-9+.-]*:)"
+ + "|uri:[
\\t]*['\"]?(?=[a-zA-Z][a-zA-Z0-9+.-]*['\"]?[ \\t\\r]*$))"
+ +
"([a-zA-Z][a-zA-Z0-9+.-]*)");
/**
* Names a component sets as exchange properties, not headers; the catalog
has no metadata for those, so the ones a
@@ -122,6 +125,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 e889e69f975a..f5d0fabbcc39 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
@@ -363,6 +363,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 6b088584c2f3..dee07617fbbc 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
@@ -625,6 +625,71 @@ class SourceValidatorEndpointTest {
assertThat(msgs).isEmpty();
}
+ /**
+ * 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 1ae0eb101002..c427b618dc51 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
@@ -902,6 +902,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 {