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("""

Reply via email to