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 a04357c4f4da CAMEL-25900: camel-jbang - AI tools: lead to the jsonpath 
filter with resultType java.util.Map for the one element of a list (#28210)
a04357c4f4da is described below

commit a04357c4f4dabe35a3723ca807971734ab53faf9
Author: Claus Ibsen <[email protected]>
AuthorDate: Wed Oct 21 21:57:00 2026 +0200

    CAMEL-25900: camel-jbang - AI tools: lead to the jsonpath filter with 
resultType java.util.Map for the one element of a list (#28210)
    
    - The source check reports an index after a jsonpath filter (an empty list) 
and && or || outside a filter (no
      JsonPath), and names the recipe: a filter with resultType: java.util.Map 
gives the element, or null.
    - camel_eval_expression of a jsonpath filter notes that a filter gives a 
list also for one match, and that a route
      gets the one element with resultType: java.util.Map.
    - camel_catalog_doc jsonpath answers the recipe in usage; 
camel_catalog_find of CamelHttpStatusCode (and similar)
      names CamelHttpResponseCode.
    - camel-bean: the hint for an OGNL read on a List leads with the jsonpath 
recipe; [0] comes after it.
    
    
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../apache/camel/language/bean/BeanExpression.java |  7 +--
 .../language/simple/SimpleSyntaxHintsTest.java     | 12 +++--
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    | 19 +++++++
 .../core/commands/ai/ExpressionEvaluator.java      | 16 ++++++
 .../dsl/jbang/core/commands/ai/JsonPathChecks.java | 61 +++++++++++++++++++++-
 .../jbang/core/commands/ai/CatalogDocsTest.java    | 17 ++++++
 .../ai/ExpressionEvaluatorLanguageTest.java        | 10 ++++
 .../jbang/core/commands/ai/JsonPathChecksTest.java | 54 +++++++++++++++++++
 8 files changed, 187 insertions(+), 9 deletions(-)

diff --git 
a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
 
b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
index d91e7ef5c00c..1a5e368d6f26 100644
--- 
a/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
+++ 
b/components/camel-bean/src/main/java/org/apache/camel/language/bean/BeanExpression.java
@@ -568,10 +568,11 @@ public class BeanExpression implements Expression, 
Predicate {
         String type = bean instanceof List ? "a List" : bean instanceof 
Object[] ? "an array" : "a Collection";
         boolean field = methodName != null && 
FIELD_NAME.matcher(methodName).matches();
         String name = field ? methodName : "field";
+        // the lookup by a field first: given [0] first, models wrote 
${body[0].orderId} and took the first element
         return " (" + type + " has no " + (field ? "method or key " + 
methodName : "such method")
-               + ": pick an element first, such as [0]." + name + ", or the 
element whose " + name
-               + " has a given value with jsonpath, such as jsonpath: 
\"$[?(@." + name + " == '${header." + name
-               + "}')]\" with resultType java.util.Map, or split it)";
+               + ": to pick the element whose " + name + " has a given value, 
use jsonpath: \"$[?(@." + name
+               + " == '${header." + name + "}')]\" with resultType 
java.util.Map (the element, or null when none"
+               + " matches); [0]." + name + " reads the first element; or 
split it)";
     }
 
     private static final Pattern FIELD_NAME = 
Pattern.compile("[A-Za-z_][A-Za-z0-9_]*");
diff --git 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
index 3bf213142d2b..fcc8ac4567aa 100644
--- 
a/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
+++ 
b/core/camel-core/src/test/java/org/apache/camel/language/simple/SimpleSyntaxHintsTest.java
@@ -242,10 +242,11 @@ public class SimpleSyntaxHintsTest extends 
ExchangeTestSupport {
         Exception e = assertThrows(Exception.class,
                 () -> 
context.resolveLanguage("simple").createExpression("${body.orderId}").evaluate(exchange,
                         String.class));
-        assertThat(e.getMessage()).contains("(a List has no method or key 
orderId: pick an element first, such as"
-                                            + " [0].orderId, or the element 
whose orderId has a given value with jsonpath,"
-                                            + " such as jsonpath: 
\"$[?(@.orderId == '${header.orderId}')]\" with"
-                                            + " resultType java.util.Map, or 
split it)");
+        // the lookup by a field first: given [0] first, models took the first 
element
+        assertThat(e.getMessage()).contains("(a List has no method or key 
orderId: to pick the element whose orderId"
+                                            + " has a given value, use 
jsonpath: \"$[?(@.orderId == '${header.orderId}')]\""
+                                            + " with resultType java.util.Map 
(the element, or null when none matches);"
+                                            + " [0].orderId reads the first 
element; or split it)");
     }
 
     @Test
@@ -254,7 +255,8 @@ public class SimpleSyntaxHintsTest extends 
ExchangeTestSupport {
         Exception e = assertThrows(Exception.class,
                 () -> 
context.resolveLanguage("simple").createExpression("${body.find(o -> o.orderId 
== 1)}")
                         .evaluate(exchange, String.class));
-        assertThat(e.getMessage()).contains("(a List has no such method: pick 
an element first, such as [0].field");
+        assertThat(e.getMessage()).contains("(a List has no such method: to 
pick the element whose field has a given"
+                                            + " value, use jsonpath");
     }
 
     @Test
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 f780f0426006..c8fee5255776 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
@@ -1089,6 +1089,12 @@ public final class CatalogDocs {
         } else {
             result.put("message", "Best match first; camel_catalog_doc gives 
the options of one");
         }
+        // a header name models write for one that has another name, such as 
CamelHttpStatusCode
+        String wrong = HeaderChecks.WRONG_NAMES.get(term.trim());
+        if (wrong != null) {
+            result.put("header", "Camel has no header " + term.trim() + ": " + 
wrong);
+            result.put("message", result.getString("message") + "; " + 
result.getString("header"));
+        }
         return result;
     }
 
@@ -1679,6 +1685,15 @@ public final class CatalogDocs {
         return answer;
     }
 
+    /**
+     * How to pick the one element of a JSON list by a field: the answer of 
camel_catalog_doc jsonpath says it first.
+     */
+    static final String JSONPATH_ONE_ELEMENT = "to pick the one element of a 
list by a field, write a filter with"
+                                               + " resultType: java.util.Map, 
such as expression: \"$[?(@.orderId =="
+                                               + " '${header.id}')]\" and 
resultType: java.util.Map: the element when one"
+                                               + " matches, null when none 
does (an index after the filter gives an empty"
+                                               + " list); camel_catalog_sample 
jsonpath shows it in a route";
+
     /** How many options of a component are listed in full when no filter is 
given (CAMEL-25541). */
     static final int DEFAULT_OPTIONS_LIMIT = 20;
 
@@ -2322,6 +2337,10 @@ public final class CatalogDocs {
         if ("simple".equals(model.getName()) || 
"csimple".equals(model.getName())) {
             result.put("syntax", SIMPLE_SYNTAX);
         }
+        if ("jsonpath".equals(model.getName())) {
+            // the one element of a list by a field, which models wrote as 
$[?(...)][0], an empty list
+            result.put("usage", JSONPATH_ONE_ELEMENT);
+        }
         JsonObject script = scriptVariables(model.getName());
         if (script != null) {
             // the variables the script sees, and how to reach the Camel API 
from them
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
index 13b177e255db..39739bf833b2 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluator.java
@@ -83,9 +83,25 @@ public final class ExpressionEvaluator {
             result.put("evaluatedIn", "a local scratch context (no integration 
selected)");
             evaluateLocally(ctx, lang, expression, body, predicate, result);
         }
+        String filter = filterNote(lang, expression, 
result.getString("status"));
+        if (filter != null) {
+            result.put("note", result.containsKey("note") ? 
result.getString("note") + "; " + filter : filter);
+        }
         return result;
     }
 
+    /**
+     * A jsonpath filter gives a list, also for one match: models picked the 
element with an index after the filter,
+     * which gives an empty list, or a made-up syntax. In a route, resultType 
java.util.Map gives the one element.
+     */
+    static String filterNote(String lang, String expression, String status) {
+        if (!"jsonpath".equals(lang) || expression == null || 
!expression.contains("[?(") || !"ok".equals(status)) {
+            return null;
+        }
+        return "a filter gives a list, also for one match; in a route, 
resultType: java.util.Map gives the one element"
+               + " it matches (null when none does), and an index after the 
filter gives an empty list";
+    }
+
     private static void evaluateInProcess(
             ToolContext ctx, String lang, String expression, String body, 
boolean predicate, JsonObject result) {
         String raw = ctx.executeAction("eval", root -> {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
index d12a0becc80a..5db46d434043 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecks.java
@@ -33,6 +33,10 @@ import static 
org.apache.camel.dsl.jbang.core.commands.ai.YamlLines.extractYamlV
 public final class JsonPathChecks {
 
     private static final Pattern COMPARISON = Pattern.compile(" 
(==|!=|<=|>=|<|>|=~|in|nin|size|empty) ");
+    // an index after a filter: $[?(@.orderId == 'ORD-1')][0]
+    private static final Pattern INDEX_AFTER_FILTER = 
Pattern.compile("\\[\\?\\(.*\\)]\\s*\\[\\s*-?\\d+\\s*]");
+    private static final String ONE_ELEMENT = "set resultType: java.util.Map 
(the element when one matches, null when"
+                                              + " none does)";
 
     private JsonPathChecks() {
     }
@@ -67,7 +71,15 @@ public final class JsonPathChecks {
                     break;
                 }
             }
-            if (text == null || text.isEmpty() || text.contains("[?(") || 
!COMPARISON.matcher(text).find()) {
+            if (text == null || text.isEmpty()) {
+                continue;
+            }
+            String filter = filterProblem(text);
+            if (filter != null) {
+                errors.add("Line " + lineNum + ": " + filter);
+                continue;
+            }
+            if (text.contains("[?(") || !COMPARISON.matcher(text).find()) {
                 continue;
             }
             if (SimpleChecks.isPredicate(catalog, lines, i, 
countLeadingSpaces(line))) {
@@ -86,4 +98,51 @@ public final class JsonPathChecks {
         }
         return errors;
     }
+
+    /**
+     * What is wrong around the filter of a path that should give the one 
element it matches. Jayway applies an index
+     * after a filter to each element the filter matched, so $[?(@.orderId == 
'ORD-1')][0] gives an empty list; and
+     * &amp;&amp; or || outside the filter is no JsonPath (500 in a REST 
route). The one element comes with resultType
+     * java.util.Map: a single match is the element, and no match null.
+     */
+    static String filterProblem(String text) {
+        if (!text.contains("[?(")) {
+            return null;
+        }
+        if (INDEX_AFTER_FILTER.matcher(text).find()) {
+            return "an index after a filter gives an empty list in JsonPath 
([?(...)][0] applies the index to each element"
+                   + " the filter matched): to get the one element, leave the 
index out and " + ONE_ELEMENT;
+        }
+        String outside = outsideFilters(text);
+        if (outside.contains("&&") || outside.contains("||")) {
+            return "&& and || work inside a filter only, such as $[?(@.status 
== 'paid' && @.qty > 0)]; to test whether"
+                   + " a filter matched, " + ONE_ELEMENT + ", and check 
${body} == null in a choice";
+        }
+        return null;
+    }
+
+    // the text with each filter [?(...)] taken out
+    private static String outsideFilters(String text) {
+        StringBuilder sb = new StringBuilder();
+        int i = 0;
+        while (i < text.length()) {
+            if (text.startsWith("[?(", i)) {
+                int depth = 0;
+                int j = i + 2;
+                for (; j < text.length(); j++) {
+                    char c = text.charAt(j);
+                    if (c == '(') {
+                        depth++;
+                    } else if (c == ')' && --depth == 0) {
+                        break;
+                    }
+                }
+                // past the closing )]
+                i = Math.min(text.length(), j + 2);
+            } else {
+                sb.append(text.charAt(i++));
+            }
+        }
+        return sb.toString();
+    }
 }
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 ae686c0d8080..3e1b624b3443 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
@@ -268,6 +268,23 @@ class CatalogDocsTest {
         }
     }
 
+    /** The one element of a JSON list by a field: models wrote $[?(...)][0], 
an empty list (the r20 REST runs). */
+    @Test
+    void theJsonPathAnswerSaysHowToPickOneElement() throws Exception {
+        JsonObject doc = catalogDoc(Map.of("name", "jsonpath"));
+        assertTrue(doc.getString("usage").contains("resultType: 
java.util.Map"), doc.getString("usage"));
+        assertTrue(doc.getString("usage").contains("an index after the filter 
gives an empty list"), doc.getString("usage"));
+    }
+
+    @Test
+    void aWrongHeaderNameIsAnsweredWithTheRightOne() {
+        org.apache.camel.catalog.CamelCatalog catalog = new 
org.apache.camel.catalog.DefaultCamelCatalog();
+        JsonObject result = CatalogDocs.find(catalog, "CamelHttpStatusCode", 
null, 10);
+        assertTrue(result.getString("header").startsWith("Camel has no header 
CamelHttpStatusCode"), result.toJson());
+        assertTrue(result.getString("header").contains("CamelHttpResponseCode 
(Exchange.HTTP_RESPONSE_CODE)"));
+        assertNull(CatalogDocs.find(catalog, "CamelHttpResponseCode", null, 
10).get("header"));
+    }
+
     @Test
     void anInterfaceNameFindsTheBuiltInBeans() {
         org.apache.camel.catalog.CamelCatalog catalog = new 
org.apache.camel.catalog.DefaultCamelCatalog();
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
index 076e5ae24910..0fd7655f1100 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/ExpressionEvaluatorLanguageTest.java
@@ -31,6 +31,16 @@ class ExpressionEvaluatorLanguageTest {
 
     private static final String STOCK = 
"[{\"sku\":\"CAMEL-MUG\",\"qty\":42},{\"sku\":\"CAMEL-CAP\",\"qty\":0}]";
 
+    /** A jsonpath filter gives a list: the answer says how a route gets the 
one element (r20 REST runs). */
+    @Test
+    void aJsonPathFilterSaysHowToGetTheOneElement() {
+        assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku == 
'CAMEL-MUG')]", "ok"))
+                .contains("resultType: java.util.Map gives the one 
element").contains("an index after the filter");
+        assertThat(ExpressionEvaluator.filterNote("jsonpath", "$.sku", 
"ok")).isNull();
+        assertThat(ExpressionEvaluator.filterNote("simple", "${body}", 
"ok")).isNull();
+        assertThat(ExpressionEvaluator.filterNote("jsonpath", "$[?(@.sku == 
'X')", "error")).isNull();
+    }
+
     @Test
     void aLanguageThatCannotBeLoadedSaysWhatToDo() {
         JsonObject result = ExpressionEvaluator.evaluate(new ToolContext(), 
"no-such-language", "x", null);
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
index 165b0d1cc2cc..ffe399a499e1 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/JsonPathChecksTest.java
@@ -55,6 +55,60 @@ class JsonPathChecksTest {
                 .contains("read as $[?(@.status == 'paid')]");
     }
 
+    /** The one order of a list by its id (the REST example of the r20 runs): 
an index after the filter gives []. */
+    @Test
+    void anIndexAfterAFilterGivesAnEmptyList() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: direct:get-order
+                      steps:
+                        - setBody:
+                            expression:
+                              jsonpath:
+                                expression: "$[?(@.orderId == 
'${header.id}')][0]"
+                """;
+        List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml, 
catalog);
+        assertThat(errors).singleElement().satisfies(e -> assertThat(e)
+                .startsWith("Line 8: an index after a filter gives an empty 
list in JsonPath")
+                .contains("leave the index out and set resultType: 
java.util.Map"));
+    }
+
+    @Test
+    void andOutsideAFilterIsNoJsonPath() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: direct:get-order
+                      steps:
+                        - choice:
+                            when:
+                              - expression:
+                                  jsonpath:
+                                    expression: "$[?(@.orderId == 
'${header.id}')] && size() > 0"
+                """;
+        List<String> errors = JsonPathChecks.validateYamlJsonPath(yaml, 
catalog);
+        assertThat(errors).singleElement().satisfies(e -> assertThat(e)
+                .startsWith("Line 9: && and || work inside a filter only")
+                .contains("set resultType: java.util.Map").contains("check 
${body} == null"));
+    }
+
+    @Test
+    void theOneElementRecipeIsFine() {
+        String yaml = """
+                - route:
+                    from:
+                      uri: direct:get-order
+                      steps:
+                        - setBody:
+                            expression:
+                              jsonpath:
+                                expression: "$[?(@.orderId == '${header.id}' 
&& @.status == 'paid')]"
+                                resultType: java.util.Map
+                """;
+        assertThat(JsonPathChecks.validateYamlJsonPath(yaml, 
catalog)).isEmpty();
+    }
+
     @Test
     void nestedPathHasNoSimpleForm() {
         String yaml = """

Reply via email to