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 d9b2a8bb3992 CAMEL-24804: camel-jbang - camel_catalog_doc for simple: 
an optionsFilter that names a function group returns that group only (#26566)
d9b2a8bb3992 is described below

commit d9b2a8bb3992ebf941a16a4c7f41ecb477ee2ff7
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Sep 17 22:37:38 2026 +0200

    CAMEL-24804: camel-jbang - camel_catalog_doc for simple: an optionsFilter 
that names a function group returns that group only (#26566)
    
    * CAMEL-24804: camel-jbang - camel_catalog_doc for simple: an optionsFilter 
that names a function group returns that group only, an operator kind that kind 
only, and a function or operator name comes first
    
    The filter was one substring match over name, display name, description and 
group, so a model that followed
    the hint and asked for the date group got every function whose description 
mentions a date, and string most
    of the catalog. A small model then reads a page of functions instead of a 
few with examples.
    
    Co-Authored-By: Claude Fable 5.1 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-24804: a function name comes first even when a group has the same 
name; bodyOneLine belongs to the core group, not a group of its own
    
    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]>
---
 .../org/apache/camel/catalog/languages/simple.json |  2 +-
 .../org/apache/camel/language/simple/simple.json   |  2 +-
 .../camel/language/simple/SimpleConstants.java     |  2 +-
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |  4 +-
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    | 53 +++++++++++++++++++---
 .../jbang/core/commands/ai/CatalogDocsTest.java    | 38 ++++++++++++++++
 6 files changed, 91 insertions(+), 10 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
index b807033f3f3d..4e7580acfd4e 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/languages/simple.json
@@ -65,7 +65,7 @@
     "bean(name.method)": { "index": 37, "kind": "function", "displayName": 
"Call Java Bean", "group": "core", "label": "core", "required": false, 
"javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Calls a Java bean. The 
name of the bean can also refer to a class name using type prefix as follows 
`bean:type:com.foo.MyClass`. If no method name is given then Camel will 
automatic attempt to find the best meth [...]
     "body": { "index": 38, "kind": "function", "displayName": "Body", "group": 
"core", "label": "core", "required": false, "javaType": "Object", "prefix": 
"${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The message body", "ognl": true, "suffix": "}", 
"examples": [ "${body} -> Hello World \/\/ when body is the string 'Hello 
World'" ] },
     "bodyAs(type)": { "index": 39, "kind": "function", "displayName": "Body 
As", "group": "core", "label": "core", "required": false, "javaType": "Object", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "Converts the message body to the given type 
(classname).", "ognl": true, "suffix": "}", "params": [ { "name": "type", 
"javaType": "String", "required": true, "description": "The target type 
classname (e.g. String, int, byte [...]
-    "bodyOneLine": { "index": 40, "kind": "function", "displayName": "Body One 
Line", "group": "function", "label": "function", "required": false, "javaType": 
"String", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Converts the body to a 
String and removes all line-breaks, so the string is in one line.", "ognl": 
false, "suffix": "}", "examples": [ "${bodyOneLine} -> Hello World \/\/ when 
body is 'Hello\\nWorld'" ] },
+    "bodyOneLine": { "index": 40, "kind": "function", "displayName": "Body One 
Line", "group": "core", "label": "core", "required": false, "javaType": 
"String", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Converts the body to a 
String and removes all line-breaks, so the string is in one line.", "ognl": 
false, "suffix": "}", "examples": [ "${bodyOneLine} -> Hello World \/\/ when 
body is 'Hello\\nWorld'" ] },
     "bodyType": { "index": 41, "kind": "function", "displayName": "Body Type", 
"group": "core", "label": "core", "required": false, "javaType": "Class", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The message body class type", "ognl": false, 
"suffix": "}", "examples": [ "${bodyType} -> java.lang.String \/\/ when body is 
a String" ] },
     "camelContext": { "index": 42, "kind": "function", "displayName": "Camel 
Context", "group": "core", "label": "core", "required": false, "javaType": 
"Object", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The Camel Context", 
"ognl": true, "suffix": "}" },
     "camelId": { "index": 43, "kind": "function", "displayName": "Camel Id", 
"group": "core", "label": "core", "required": false, "javaType": "String", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The name of the CamelContext", "ognl": false, 
"suffix": "}", "examples": [ "${camelId} -> camel-1" ] },
diff --git 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
index b807033f3f3d..4e7580acfd4e 100644
--- 
a/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
+++ 
b/core/camel-core-languages/src/generated/resources/META-INF/org/apache/camel/language/simple/simple.json
@@ -65,7 +65,7 @@
     "bean(name.method)": { "index": 37, "kind": "function", "displayName": 
"Call Java Bean", "group": "core", "label": "core", "required": false, 
"javaType": "Object", "prefix": "${", "deprecated": false, "deprecationNote": 
"", "autowired": false, "secret": false, "description": "Calls a Java bean. The 
name of the bean can also refer to a class name using type prefix as follows 
`bean:type:com.foo.MyClass`. If no method name is given then Camel will 
automatic attempt to find the best meth [...]
     "body": { "index": 38, "kind": "function", "displayName": "Body", "group": 
"core", "label": "core", "required": false, "javaType": "Object", "prefix": 
"${", "deprecated": false, "deprecationNote": "", "autowired": false, "secret": 
false, "description": "The message body", "ognl": true, "suffix": "}", 
"examples": [ "${body} -> Hello World \/\/ when body is the string 'Hello 
World'" ] },
     "bodyAs(type)": { "index": 39, "kind": "function", "displayName": "Body 
As", "group": "core", "label": "core", "required": false, "javaType": "Object", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "Converts the message body to the given type 
(classname).", "ognl": true, "suffix": "}", "params": [ { "name": "type", 
"javaType": "String", "required": true, "description": "The target type 
classname (e.g. String, int, byte [...]
-    "bodyOneLine": { "index": 40, "kind": "function", "displayName": "Body One 
Line", "group": "function", "label": "function", "required": false, "javaType": 
"String", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Converts the body to a 
String and removes all line-breaks, so the string is in one line.", "ognl": 
false, "suffix": "}", "examples": [ "${bodyOneLine} -> Hello World \/\/ when 
body is 'Hello\\nWorld'" ] },
+    "bodyOneLine": { "index": 40, "kind": "function", "displayName": "Body One 
Line", "group": "core", "label": "core", "required": false, "javaType": 
"String", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "Converts the body to a 
String and removes all line-breaks, so the string is in one line.", "ognl": 
false, "suffix": "}", "examples": [ "${bodyOneLine} -> Hello World \/\/ when 
body is 'Hello\\nWorld'" ] },
     "bodyType": { "index": 41, "kind": "function", "displayName": "Body Type", 
"group": "core", "label": "core", "required": false, "javaType": "Class", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The message body class type", "ognl": false, 
"suffix": "}", "examples": [ "${bodyType} -> java.lang.String \/\/ when body is 
a String" ] },
     "camelContext": { "index": 42, "kind": "function", "displayName": "Camel 
Context", "group": "core", "label": "core", "required": false, "javaType": 
"Object", "prefix": "${", "deprecated": false, "deprecationNote": "", 
"autowired": false, "secret": false, "description": "The Camel Context", 
"ognl": true, "suffix": "}" },
     "camelId": { "index": 43, "kind": "function", "displayName": "Camel Id", 
"group": "core", "label": "core", "required": false, "javaType": "String", 
"prefix": "${", "deprecated": false, "deprecationNote": "", "autowired": false, 
"secret": false, "description": "The name of the CamelContext", "ognl": false, 
"suffix": "}", "examples": [ "${camelId} -> camel-1" ] },
diff --git 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
index e733d619a3cc..e5b61924a59a 100644
--- 
a/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
+++ 
b/core/camel-core-languages/src/main/java/org/apache/camel/language/simple/SimpleConstants.java
@@ -230,7 +230,7 @@ public final class SimpleConstants {
     public static final String BODY_AS = "bodyAs(type)";
 
     @Metadata(description = "Converts the body to a String and removes all 
line-breaks, so the string is in one line.",
-              javaType = "String", label = "function",
+              javaType = "String", label = "core",
               examples = { "${bodyOneLine} -> Hello World // when body is 
'Hello\\nWorld'" })
     public static final String BODY_ONE_LINE = "bodyOneLine";
 
diff --git a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc 
b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
index bb95dd07da4a..23a95e7f3ad6 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -269,7 +269,9 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   Maven coordinates, and for a component the rules of its endpoint URI spelled 
out (which
   options are path parts, the YAML `uri` plus `parameters` form, placeholders, 
`RAW()`). For the `simple`
   language the syntax rules, functions and operators (their count and names by 
group, or with `optionsFilter`
-  the matching ones with parameters and examples), and `docPage` serves the 
functions, operators, OGNL and
+  the matching ones with parameters and examples: a group name such as `date` 
gives that group only, an
+  operator kind such as `logical` that kind only, a function or operator name 
comes first, and any other word
+  matches the names and descriptions), and `docPage` serves the functions, 
operators, OGNL and
   advanced pages. With `endpoint` it checks a URI against the catalog: unknown 
options with the closest real
   names, invalid values, missing path parts, consumer options on a producer 
endpoint. `includeHeaders=true`
   adds the message headers of a component (the `CamelXxx` names, their 
constants, types and consumer or
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 9ef75c5527be..717e5bb60732 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
@@ -1084,15 +1084,30 @@ public final class CatalogDocs {
         if (functions != null && !functions.isEmpty()) {
             result.put("functionCount", functions.size());
             if (filter != null) {
+                // a function name comes first, even when a group has the same 
name; a group name is then that group
+                // and nothing else (date is two functions, not every function 
whose description mentions a date);
+                // the rest is a word match
+                boolean group = functions.stream().anyMatch(fn -> 
filter.equalsIgnoreCase(fn.getGroup()));
+                JsonArray exact = new JsonArray();
                 JsonArray arr = new JsonArray();
                 for (LanguageModel.LanguageFunctionModel fn : functions) {
-                    if (matchesOptionFilter(fn, filter)
+                    if (isFunctionName(fn, filter)) {
+                        exact.add(functionToJson(fn));
+                    } else if (group) {
+                        if (filter.equalsIgnoreCase(fn.getGroup())) {
+                            arr.add(functionToJson(fn));
+                        }
+                    } else if (matchesOptionFilter(fn, filter)
                             || (fn.getDisplayName() != null && 
fn.getDisplayName().toLowerCase().contains(filter))) {
                         arr.add(functionToJson(fn));
                     }
                 }
-                result.put("functions", arr);
-                result.put("matchedFunctions", arr.size());
+                exact.addAll(arr);
+                result.put("functions", exact);
+                result.put("matchedFunctions", exact.size());
+                if (group) {
+                    result.put("functionGroup", filter);
+                }
             } else {
                 Map<String, JsonArray> groups = new TreeMap<>();
                 for (LanguageModel.LanguageFunctionModel fn : functions) {
@@ -1109,15 +1124,28 @@ public final class CatalogDocs {
         if (operators != null && !operators.isEmpty()) {
             result.put("operatorCount", operators.size());
             if (filter != null) {
+                // an operator kind (binary, logical, unary...) is that kind 
only; an operator itself comes first
+                boolean kind = operators.stream().anyMatch(op -> 
filter.equalsIgnoreCase(op.getOperatorKind()));
+                JsonArray exact = new JsonArray();
                 JsonArray arr = new JsonArray();
                 for (LanguageModel.LanguageOperatorModel op : operators) {
-                    if (matchesOptionFilter(op, filter)
+                    if (kind) {
+                        if (filter.equalsIgnoreCase(op.getOperatorKind())) {
+                            arr.add(operatorToJson(op));
+                        }
+                    } else if (filter.equalsIgnoreCase(op.getName())) {
+                        exact.add(operatorToJson(op));
+                    } else if (matchesOptionFilter(op, filter)
                             || (op.getOperatorKind() != null && 
op.getOperatorKind().toLowerCase().contains(filter))) {
                         arr.add(operatorToJson(op));
                     }
                 }
-                result.put("operators", arr);
-                result.put("matchedOperators", arr.size());
+                exact.addAll(arr);
+                result.put("operators", exact);
+                result.put("matchedOperators", exact.size());
+                if (kind) {
+                    result.put("operatorKind", filter);
+                }
             } else {
                 JsonArray syntaxes = new JsonArray();
                 for (LanguageModel.LanguageOperatorModel op : operators) {
@@ -1128,6 +1156,19 @@ public final class CatalogDocs {
         }
     }
 
+    /**
+     * Whether the filter is the function's name: {@code random} or {@code 
random(min,max)} for {@code random(min,max)}.
+     */
+    private static boolean isFunctionName(LanguageModel.LanguageFunctionModel 
fn, String filter) {
+        String name = fn.getName();
+        if (name == null) {
+            return false;
+        }
+        int paren = name.indexOf('(');
+        String bare = paren > 0 ? name.substring(0, paren) : name;
+        return filter.equalsIgnoreCase(name) || filter.equalsIgnoreCase(bare);
+    }
+
     private static JsonObject 
functionToJson(LanguageModel.LanguageFunctionModel fn) {
         JsonObject o = new JsonObject();
         o.put("name", fn.getName());
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 d8fc384d87b1..97cbf8277fbe 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
@@ -179,6 +179,44 @@ class CatalogDocsTest {
         assertEquals("LHS == RHS", eq.getString("syntax"));
     }
 
+    @Test
+    void aGroupNameReturnsTheGroupOnlyAndAFunctionNameComesFirst() throws 
Exception {
+        // CAMEL-24804: date is a group of two functions, and two more mention 
a date in their description
+        JsonObject dates = catalogDoc(Map.of("name", "simple", "kind", 
"language", "optionsFilter", "date"));
+        assertEquals("date", dates.getString("functionGroup"));
+        assertEquals(2, dates.getInteger("matchedFunctions"));
+        dates.getCollection("functions").forEach(fn -> assertEquals("date", 
((JsonObject) fn).getString("group")));
+
+        // string is a group of a dozen or so, while most descriptions say 
"string" somewhere
+        JsonObject strings = catalogDoc(Map.of("name", "simple", "kind", 
"language", "optionsFilter", "string"));
+        assertEquals("string", strings.getString("functionGroup"));
+        assertTrue(strings.getInteger("matchedFunctions") < 25,
+                "the group, not the catalog: " + 
strings.getInteger("matchedFunctions"));
+        strings.getCollection("functions").forEach(fn -> 
assertEquals("string", ((JsonObject) fn).getString("group")));
+
+        // a function name comes first, with or without its parameters, then 
the word matches
+        for (String name : List.of("random", "RANDOM", "random(min,max)")) {
+            JsonObject random = catalogDoc(Map.of("name", "simple", "kind", 
"language", "optionsFilter", name));
+            assertNull(random.get("functionGroup"));
+            JsonObject first = (JsonObject) 
random.getCollection("functions").iterator().next();
+            assertEquals("random(min,max)", first.getString("name"), name);
+        }
+
+        // a function name comes first even when a group has the same name
+        JsonObject function = catalogDoc(Map.of("name", "simple", "kind", 
"language", "optionsFilter", "function"));
+        assertEquals("function(name,exp)",
+                ((JsonObject) 
function.getCollection("functions").iterator().next()).getString("name"));
+
+        // an operator kind is that kind only, and an operator itself comes 
first
+        JsonObject logical = catalogDoc(Map.of("name", "simple", "kind", 
"language", "optionsFilter", "logical"));
+        assertEquals("logical", logical.getString("operatorKind"));
+        assertTrue(logical.getInteger("matchedOperators") >= 2);
+        logical.getCollection("operators").forEach(op -> 
assertEquals("logical", ((JsonObject) op).getString("kind")));
+        JsonObject eq = catalogDoc(Map.of("name", "simple", "kind", 
"language", "optionsFilter", "=="));
+        assertNull(eq.get("operatorKind"));
+        assertEquals("==", ((JsonObject) 
eq.getCollection("operators").iterator().next()).getString("name"));
+    }
+
     @Test
     void anInterfaceNameFindsTheBuiltInBeans() {
         org.apache.camel.catalog.CamelCatalog catalog = new 
org.apache.camel.catalog.DefaultCamelCatalog();

Reply via email to