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 5a491e0b8305 CAMEL-26412: camel-jbang - AI tools: the error handler
says it acts on an exception only, a pollEnrich of a file gives the file, and
two REST verbs on one path are caught (#28718)
5a491e0b8305 is described below
commit 5a491e0b8305a1337ddd26a1e3ff68788044fa09
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 30 10:46:44 2026 +0100
CAMEL-26412: camel-jbang - AI tools: the error handler says it acts on an
exception only, a pollEnrich of a file gives the file, and two REST verbs on
one path are caught (#28718)
From the r24 run of the local-model benchmark:
- the camel_catalog_sample and camel_catalog_doc answers for errorHandler,
its kinds and onException say they act only when a step fails with an
exception: a reply that means no ("not enough") must be thrown
- a pollEnrich with no variableReceive and no aggregation strategy gives
the body the consumer gives: for file, the file as it was read, so a field
read after it is reported at write time
- two operations of one rest with the same verb and path are reported at
write time, before Camel stops loading the routes with "Duplicate verb"
- a quoted value followed by a stray : gets the line to write, not the
message about concatenation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
.../dsl/jbang/core/commands/ai/CatalogDocs.java | 3 ++
.../dsl/jbang/core/commands/ai/CatalogSamples.java | 20 ++++++++
.../jbang/core/commands/ai/CatalogDocsTest.java | 6 +++
.../jbang/core/commands/ai/CatalogSamplesTest.java | 11 +++++
.../camel/dsl/yaml/validator/TextBodyFlow.java | 22 +++++++++
.../camel/dsl/yaml/validator/YamlValidator.java | 51 ++++++++++++++++++++
.../camel/dsl/yaml/validator/TextBodyFlowTest.java | 37 +++++++++++++++
.../validator/YamlValidatorPropertyHintTest.java | 54 ++++++++++++++++++++++
8 files changed, 204 insertions(+)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
index d44571f18cb2..b04d7d12c65b 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocs.java
@@ -2710,6 +2710,9 @@ public final class CatalogDocs {
if (placement != null) {
result.put("placement", placement);
}
+ if (CatalogSamples.actsOnExceptions(model.getName())) {
+ result.put("trigger", CatalogSamples.ON_EXCEPTION_ONLY);
+ }
if (scope != OptionScope.NONE) {
putOptions(result, model.getOptions(), filter, scope);
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
index fdc543f29d8b..42c2055b9d3a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
@@ -644,6 +644,9 @@ public final class CatalogSamples {
answer.put("note", "'" + given + "' is done with the " + key + "
EIP");
}
answer.put("placement", placement(key));
+ if (actsOnExceptions(key)) {
+ answer.put("trigger", ON_EXCEPTION_ONLY);
+ }
List<Map<String, String>> list = fromCatalog(catalog, "eip",
key).samples();
if (list.isEmpty()) {
list = samples().getOrDefault(key, List.of());
@@ -813,6 +816,23 @@ public final class CatalogSamples {
static final Set<String> ERROR_HANDLERS = Set.of("errorHandler",
"deadLetterChannel", "defaultErrorHandler",
"noErrorHandler", "jtaTransactionErrorHandler",
"springTransactionErrorHandler", "refErrorHandler");
+ /**
+ * When the error handler and onException act, for their sample and doc
answers. r24 jms step 3: told to move an
+ * order the stock service cannot reserve to a dead letter queue, the
model added the dead letter channel and no
+ * throw, in 7 runs of 7, so the answer "not enough" never reached it. The
first sample throws, but nothing said
+ * why.
+ */
+ static final String ON_EXCEPTION_ONLY = "the error handler and onException
act only when a step fails with an"
+ + " exception. A reply or a value
that means no, such as \"not enough\" or"
+ + " \"declined\", is a normal
message to them: test it (filter or choice) and"
+ + " throw an exception there
(throwException), so the redelivery and the"
+ + " dead letter channel apply to
it";
+
+ /** Whether the answer for this EIP says when it acts: the error handlers
that handle, and onException. */
+ static boolean actsOnExceptions(String name) {
+ return "onException".equals(name) || ERROR_HANDLERS.contains(name) &&
!"noErrorHandler".equals(name);
+ }
+
static String placement(String key) {
if (ERROR_HANDLERS.contains(key)) {
// a step it is not, and it has two places (CAMEL-25370)
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
index ab7d10e13e1a..f3c600932d83 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocsTest.java
@@ -812,6 +812,12 @@ class CatalogDocsTest {
CamelCatalog catalog = new DefaultCamelCatalog();
var doc = CatalogDocs.catalogDoc(catalog, "errorHandler", null, "eip",
null, null, false, false, null);
assertTrue(doc.getString("placement").startsWith("errorHandler: with
the kind"), doc.toJson());
+ // r24 jms step 3: when it acts, for the error handlers and
onException only
+ assertTrue(doc.getString("trigger").contains("only when a step fails
with an exception"), doc.toJson());
+ doc = CatalogDocs.catalogDoc(catalog, "onException", null, "eip",
null, null, false, false, null);
+ assertTrue(doc.getString("trigger").contains("throwException"),
doc.toJson());
+ doc = CatalogDocs.catalogDoc(catalog, "noErrorHandler", null, "eip",
null, null, false, false, null);
+ assertFalse(doc.containsKey("trigger"), doc.toJson());
doc = CatalogDocs.catalogDoc(catalog, "routeConfiguration", null,
"eip", null, null, false, false, null);
assertTrue(doc.getString("placement").startsWith("top-level entry"),
doc.toJson());
doc = CatalogDocs.catalogDoc(catalog, "when", null, "eip", null, null,
false, false, null);
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
index 9ae56ca3e31d..77c7dabfc6c4 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamplesTest.java
@@ -498,6 +498,17 @@ public class CatalogSamplesTest {
assertThat(o.getString("note")).contains("the sample shows it in
place");
}
+ /** r24 jms step 3: the error handler answers say that only an exception
reaches them, and what to do with a no. */
+ @Test
+ void theErrorHandlerSaysItActsOnlyOnAnException() {
+ for (String name : List.of("errorHandler", "deadLetterChannel",
"onException")) {
+ assertThat(CatalogSamples.sample(name,
1).getString("trigger")).as(name)
+ .contains("only when a step fails with an
exception").contains("\"not enough\"")
+ .contains("throwException");
+ }
+ assertThat(CatalogSamples.sample("split", 1).get("trigger")).isNull();
+ }
+
/** CAMEL-25370: a kind the samples do not show is not said to be shown;
where it goes and its options are. */
@Test
void anErrorHandlerKindTheSamplesDoNotShow() {
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
index b0578c59170d..8af810156954 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/main/java/org/apache/camel/dsl/yaml/validator/TextBodyFlow.java
@@ -574,6 +574,15 @@ final class TextBodyFlow {
}
return producerBody(out, "transformDataType: " +
toTypeOf(value) + " makes it");
}
+ case "pollEnrich", "poll" -> {
+ // with no variableReceive and no aggregation strategy the
polled message is the body: a file as it was
+ // read (r24 enrich step 2: ${body[name]} after a pollEnrich
of the customer file, which the validator
+ // took for unknown)
+ String uri = sendUri(name, value);
+ return uri != null && !mergesTheReply(value)
+ ? consumerBody(uri, endpointNode(uri), w.bodyTypes(),
name + ": " + normalize(uri) + " gives it")
+ : null;
+ }
case "kamelet" -> {
// what the Kamelet leaves, as the transformer of its out data
type says
String kamelet = kameletName(value);
@@ -836,6 +845,19 @@ final class TextBodyFlow {
};
}
+ /** Whether a pollEnrich merges the polled message with the message, so
the body after it is not known. */
+ private static boolean mergesTheReply(JsonNode value) {
+ if (value == null || !value.isObject()) {
+ return false;
+ }
+ for (var it = value.fieldNames(); it.hasNext();) {
+ if (it.next().startsWith("aggregationStrategy")) {
+ return true;
+ }
+ }
+ return false;
+ }
+
/** What a setVariable sets: the body for simple ${body}, text for a
constant or a simple template. */
private static Body setVariableBody(JsonNode value, Body body) {
JsonNode node = value.has("expression") ? value.get("expression") :
value;
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 cd1d75848cc6..73a0cc60c997 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
@@ -304,6 +304,11 @@ public class YamlValidator {
String message = "\"".equals(tail)
? "line " + last + ": the value of " + key + "
ends with two double quotes; remove the"
+ " extra one and write the line as " +
t.strip().substring(0, t.strip().length() - 1)
+ // a line copied with the colon of the next key:
expression: "$[?(@.id == 'a')]": (r24)
+ : ":".equals(tail)
+ ? "line " + last + ": the value of " + key
+ " ends with a : after its closing quote;"
+ + " remove it and write the line as "
+ + t.strip().substring(0,
t.strip().length() - 1).stripTrailing()
: "line " + last + ": the value of " + key + "
continues after its closing quote"
+ " (\"...\" + ...): a YAML value is one string,
there is no concatenation; a"
+ " log message is a simple expression, write it
as one quoted text such as"
@@ -891,6 +896,7 @@ public class YamlValidator {
checkSimpleSyntaxInScripts(camelOnly, new
NodePath(PathType.JSON_POINTER), errors);
checkDynamicUri(camelOnly, new NodePath(PathType.JSON_POINTER),
errors);
checkVariableSendOfAHeader(camelOnly, new
NodePath(PathType.JSON_POINTER), errors);
+ checkDuplicateRestVerbs(camelOnly, new
NodePath(PathType.JSON_POINTER), errors);
// where the body comes from, across the routes of the file
(CAMEL-24844)
BodyTypeFlow.check(camelOnly, new NodePath(PathType.JSON_POINTER),
errors, facts.bodylessEndpoints(),
bodyTypes());
@@ -1133,6 +1139,51 @@ public class YamlValidator {
}
}
+ /** The verbs of the rest DSL, each a list of operations under rest:. */
+ private static final List<String> REST_VERBS = List.of("get", "post",
"put", "delete", "patch", "head");
+
+ /**
+ * Two operations of one rest with the same verb and the same path: Camel
stops loading the routes with "Duplicate
+ * verb detected in rest-dsl" (RestDefinition.validateUniquePaths), which
the schema cannot see. r24 rest-code-first
+ * step 1: a second get with no path: was added for one order, and the
reload failed.
+ */
+ void checkDuplicateRestVerbs(JsonNode target, NodePath path, List<Error>
errors) {
+ if (target == null || !target.isArray()) {
+ return;
+ }
+ for (int i = 0; i < target.size(); i++) {
+ JsonNode rest = target.get(i).get("rest");
+ if (rest == null || !rest.isObject()) {
+ continue;
+ }
+ for (String verb : REST_VERBS) {
+ JsonNode operations = rest.get(verb);
+ if (operations == null || !operations.isArray()) {
+ continue;
+ }
+ Set<String> paths = new HashSet<>();
+ for (int j = 0; j < operations.size(); j++) {
+ JsonNode p = operations.get(j).get("path");
+ // as RestDefinition: the verb alone when there is no path
+ String key = p != null && p.isValueNode() ? ":" +
p.asText() : "";
+ if (!paths.add(key)) {
+ String what = key.isEmpty() ? "no path:" : "the path "
+ p.asText();
+ errors.add(Error.builder()
+ .keyword("type")
+
.instanceLocation(path.append(i).append("rest").append(verb).append(j))
+ .messageKey("type")
+ .format(new MessageFormat("{0}"))
+ .arguments("rest: a second " + verb + " with "
+ what + ", like one before it -"
+ + " Camel stops loading the routes
with \"Duplicate verb detected in"
+ + " rest-dsl\"; give each " + verb
+ " its own path, such as path: /{id}"
+ + " for one element (its value is
then in ${header.id})")
+ .build());
+ }
+ }
+ }
+ }
+ }
+
private static void reportVariableSend(
JsonNode node, NodePath path, Set<String> headers, Set<String>
variables, String routeId, List<Error> errors) {
if (node == null || !node.isContainerNode()) {
diff --git
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
index af3dfc683a71..97dc90060d63 100644
---
a/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
+++
b/dsl/camel-yaml-dsl/camel-yaml-dsl-validator/src/test/java/org/apache/camel/dsl/yaml/validator/TextBodyFlowTest.java
@@ -2882,6 +2882,43 @@ public class TextBodyFlowTest {
assertThat(lines(variable.replace("variableSend: order",
"variableSend: other"))).isEmpty();
}
+ @Test
+ public void testAFieldReadOnTheFileAPollEnrichGives() {
+ // r24 enrich step 2: with no variableReceive the polled customer file
is the body, read as if it were parsed
+ String yaml = """
+ - route:
+ id: enrich
+ from:
+ uri: direct:enrich
+ steps:
+ - pollEnrich:
+ expression:
+ simple:
+ expression:
"file:customers?fileName=${header.customer}.json&noop=true"
+ timeout: 5000
+ - log:
+ message: "Customer ${body[name]}"
+ """;
+ assertThat(lines(yaml)).singleElement().satisfies(m -> {
+ assertThat(m).contains("route enrich: ");
+ assertThat(m).contains("${body[name]}");
+ assertThat(m).contains("the body here is the file as it was read
(a GenericFile)");
+ assertThat(m).contains("pollEnrich: file:customers gives it");
+ });
+ // what the file has, and the file once unmarshalled
+ assertThat(lines(yaml.replace("${body[name]}",
"${body.fileName}"))).isEmpty();
+ assertThat(lines(yaml.replace("""
+ - log:
+ """, """
+ - unmarshal:
+ json: {}
+ - log:
+ """))).isEmpty();
+ // an aggregation strategy merges the reply with the message: nothing
is known after it
+ assertThat(lines(yaml.replace("timeout: 5000",
+ "timeout: 5000\n aggregationStrategy:
\"#mergeCustomer\""))).isEmpty();
+ }
+
@Test
public void testAFieldReadOnAVariableThatHoldsTheFile() {
// what the benchmark wrote (r22): the customer file is polled into a
variable, and read as if it were parsed
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 64540c5c9e82..6c32c385cbd8 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
@@ -974,6 +974,60 @@ public class YamlValidatorPropertyHintTest {
.contains("no concatenation");
}
+ @Test
+ public void testAColonAfterTheClosingQuoteIsNamed() throws Exception {
+ // r24 rest-code-first step 1: the jsonpath line copied with the colon
of a key after it, told 5 times that a
+ // YAML value has no concatenation
+ List<Error> errors = validator.validate("""
+ - from:
+ uri: timer:tick
+ steps:
+ - setBody:
+ jsonpath:
+ expression: "$[?(@.orderId ==
'${header.orderId}')]":
+ """);
+ assertThat(errors).hasSize(1);
+ assertThat(errors.get(0).getMessage())
+ .startsWith("line 6: the value of expression ends with a :
after its closing quote")
+ .endsWith("write the line as expression: \"$[?(@.orderId ==
'${header.orderId}')]\"")
+ .doesNotContain("concatenation");
+ }
+
+ @Test
+ public void testTwoGetsWithTheSamePathInOneRest() throws Exception {
+ // r24 rest-code-first step 1: a second get with no path:, which Camel
refuses when it loads the routes
+ String yaml = """
+ - rest:
+ path: /orders
+ get:
+ - to: direct:list-orders
+ - to: direct:get-order
+ - route:
+ from:
+ uri: direct:list-orders
+ steps:
+ - log: "list"
+ - route:
+ from:
+ uri: direct:get-order
+ steps:
+ - log: "one"
+ """;
+ List<Error> errors = validator.validate(yaml);
+ assertThat(errors).singleElement().satisfies(e ->
assertThat(e.getMessage())
+ .contains("rest: a second get with no
path:").contains("Duplicate verb detected in rest-dsl")
+ .contains("path: /{id}"));
+ String oneOrder = " - to: direct:get-order";
+ String withPath = " - path: /{id}\n to: direct:get-order";
+ assertThat(validator.validate(yaml.replace(oneOrder,
withPath))).isEmpty();
+ // the same path twice
+ assertThat(validator.validate(yaml.replace(oneOrder,
withPath).replace(" - to: direct:list-orders",
+ " - path: /{id}\n to:
direct:list-orders"))).singleElement()
+ .satisfies(e -> assertThat(e.getMessage()).contains("a second
get with the path /{id}"));
+ // a get and a post with no path are two verbs, which is fine
+ assertThat(validator.validate(yaml.replace(oneOrder, " post:\n" +
oneOrder))).isEmpty();
+ }
+
@Test
public void testLanguageAsAPropertySaysToUseItAsTheKey() throws Exception {
List<Error> errors = validator.validate("""