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 f7953d54b246 CAMEL-25377: camel-jbang - camel_catalog_doc nested 
options: the YAML key of an element, and the options of the error handler kinds 
(#27582)
f7953d54b246 is described below

commit f7953d54b246745d0caa41847bc5b88878d682ce
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Oct 8 19:41:34 2026 +0200

    CAMEL-25377: camel-jbang - camel_catalog_doc nested options: the YAML key 
of an element, and the options of the error handler kinds (#27582)
    
    * CAMEL-25377: camel-jbang - camel_catalog_doc nested options: the YAML key 
of an element, and the options of the error handler kinds
    
    When optionsFilter matches none of an EIP's own options, camel_catalog_doc
    lists the options of the EIP's elements under nestedOptions. Two gaps:
    
    - The element was named by its catalog name, which is not always its YAML
      key: routeTemplate's templateParameter is written as parameters:. The key
      now comes from the YAML DSL model the catalog ships, matched by name or
      by the title of the element's model; it falls back to the catalog name.
    - errorHandler had no nested options: they sit in the models of its kinds
      behind errorHandlerType. The kinds are now searched, so deadLetterUri is
      found under deadLetterChannel. An option that several kinds have is
      listed once, with the other kinds in alsoUnder (29 entries become 8 for
      "redelivery").
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    * CAMEL-25377: parentheses around the title match
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Claude-Session: https://claude.ai/code/session_01STT6whBgK1AqsSsUKrnE8m
    
    ---------
    
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../modules/ROOT/pages/camel-jbang-mcp.adoc        |   7 +-
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    | 126 ++++++++++++++++++---
 .../jbang/core/commands/ai/CatalogDocsTest.java    |  42 +++++++
 3 files changed, 156 insertions(+), 19 deletions(-)

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 6884bc17ac33..7b51b46e0c83 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-jbang-mcp.adoc
@@ -283,8 +283,11 @@ project `directory` as an argument, the runtime tools take 
the integration `name
   ones and says how many with `omittedOptions`, `required` lists the required 
ones, `all` everything, `false`
   none; `optionsFilter` matches a keyword in the option names, descriptions 
and groups and searches all
   options. When it matches none of an EIP's own options, the options of the 
EIP's elements are searched and
-  listed under `nestedOptions` with the element they go under (`logStackTrace` 
of `onException` is an option
-  of its `redeliveryPolicy`). An element written as one of several kinds has 
`oneOf` with the kinds and a
+  listed under `nestedOptions` with the YAML key of the element they go under 
(`logStackTrace` of `onException`
+  is an option of its `redeliveryPolicy`; the template parameters of 
`routeTemplate` go under `parameters`). For
+  an element written as one of several kinds, the options of the kinds are 
searched: `deadLetterUri` of
+  `errorHandler` goes under `deadLetterChannel`, and an option that several 
kinds have lists the others in
+  `alsoUnder`. An element written as one of several kinds has `oneOf` with the 
kinds and a
   `yaml` hint: the kind is the key (`errorHandler: {deadLetterChannel: ...}`, 
not `errorHandlerType:`).
   With `kind=api` (or a class name such as `Exchange`) it answers with the 
compact API reference of the core
   classes a bean or script calls: `Exchange`, `Message`, `CamelContext`, 
`Registry`, `ProducerTemplate`,
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 8cabb8f9740a..bebdb0eb51f8 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
@@ -16,10 +16,13 @@
  */
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
+import java.io.InputStream;
+import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
@@ -41,6 +44,7 @@ import org.apache.camel.tooling.model.LanguageModel;
 import org.apache.camel.tooling.model.PojoBeanModel;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
 
 /**
  * The catalog documentation an AI agent authoring an integration asks for: a 
component, data format, language or EIP
@@ -457,33 +461,124 @@ public final class CatalogDocs {
     private static void putNestedOptions(
             CamelCatalog catalog, EipModel model, String filter, OptionScope 
scope, JsonObject result) {
         JsonArray nested = new JsonArray();
-        String example = null;
+        String[] example = new String[1];
+        JsonObject yamlNodes = null;
         for (BaseOptionModel opt : model.getOptions()) {
             if (!"element".equals(opt.getKind())) {
                 continue;
             }
+            if (isWrittenAsKind(opt)) {
+                // the element is written as one of its kinds, and the options 
are the ones of the kinds: the error
+                // handler's deadLetterUri is errorHandler: 
{deadLetterChannel: {deadLetterUri: ...}} (CAMEL-25377).
+                // An option that several kinds have is listed once, with the 
other kinds in alsoUnder.
+                Map<String, JsonObject> byName = new LinkedHashMap<>();
+                for (String kind : opt.getOneOfs()) {
+                    EipModel element = catalog.eipModel(kind);
+                    if (element == null || element.getOptions() == null) {
+                        continue;
+                    }
+                    JsonArray found = new JsonArray();
+                    addNestedOptions(element, kind, false, filter, scope, 
found, example);
+                    for (Object f : found) {
+                        JsonObject o = (JsonObject) f;
+                        JsonObject first = 
byName.putIfAbsent(o.getString("name"), o);
+                        if (first != null) {
+                            JsonArray also = (JsonArray) 
first.computeIfAbsent("alsoUnder", k -> new JsonArray());
+                            also.add(kind);
+                        }
+                    }
+                }
+                nested.addAll(byName.values());
+                continue;
+            }
             EipModel element = catalog.eipModel(opt.getName());
             // the option must hold that model, not one that only shares its 
name (templatedRoute.bean is a bean
             // factory, not the bean EIP)
             if (element == null || element.getOptions() == null || !holds(opt, 
element)) {
                 continue;
             }
-            boolean list = "array".equals(opt.getType());
-            for (BaseOptionModel inner : element.getOptions()) {
-                if (matchesOptionFilter(inner, filter) && scope.accepts(inner, 
filter)) {
-                    JsonObject o = optionToJson(inner, null);
-                    o.put("under", opt.getName());
-                    nested.add(o);
-                    if (example == null) {
-                        example = opt.getName() + ": " + (list ? "[{" : "{") + 
inner.getName() + ": ..." + (list ? "}]" : "}");
-                    }
-                }
+            if (yamlNodes == null) {
+                yamlNodes = yamlDslNodes(catalog);
             }
+            String key = yamlKey(yamlNodes, model.getName(), opt.getName(), 
element);
+            addNestedOptions(element, key, "array".equals(opt.getType()), 
filter, scope, nested, example);
         }
         if (!nested.isEmpty()) {
             result.put("nestedOptions", nested);
             result.put("nestedHint", "options of an element of " + 
model.getName() + ": write them under that element,"
-                                     + " for example " + example);
+                                     + " for example " + example[0]);
+        }
+    }
+
+    private static void addNestedOptions(
+            EipModel element, String under, boolean list, String filter, 
OptionScope scope, JsonArray nested,
+            String[] example) {
+        for (BaseOptionModel inner : element.getOptions()) {
+            if (matchesOptionFilter(inner, filter) && scope.accepts(inner, 
filter)) {
+                JsonObject o = optionToJson(inner, null);
+                o.put("under", under);
+                nested.add(o);
+                if (example[0] == null) {
+                    example[0] = under + ": " + (list ? "[{" : "{") + 
inner.getName() + ": ..." + (list ? "}]" : "}");
+                }
+            }
+        }
+    }
+
+    /**
+     * Whether the option is a single element written as one of its kinds, not 
under its own name: the error handler is
+     * errorHandler: {deadLetterChannel: {...}}, never errorHandlerType:. Not 
a list such as outputs (written under
+     * steps:), nor an element whose kind is its own name (when).
+     */
+    private static boolean isWrittenAsKind(BaseOptionModel opt) {
+        return "element".equals(opt.getKind()) && 
"object".equals(opt.getType()) && opt.getOneOfs() != null
+                && !opt.getOneOfs().isEmpty() && 
!opt.getOneOfs().contains(opt.getName());
+    }
+
+    /**
+     * The YAML DSL key of an element of an EIP, from the YAML DSL model of 
the catalog: the catalog names an element
+     * after its model, which is not always its YAML key (routeTemplate's 
templateParameter is written as parameters:).
+     * The element is the YAML child of that name, else the one whose model 
has the same title. Falls back to the
+     * catalog name.
+     */
+    static String yamlKey(JsonObject yamlNodes, String eip, String name, 
EipModel element) {
+        JsonObject node = yamlNodes != null ? (JsonObject) yamlNodes.get(eip) 
: null;
+        JsonArray children = node != null ? (JsonArray) node.get("children") : 
null;
+        if (children == null) {
+            return name;
+        }
+        for (Object c : children) {
+            if (name.equals(((JsonObject) c).getString("name"))) {
+                return name;
+            }
+        }
+        for (Object c : children) {
+            JsonObject child = (JsonObject) c;
+            String ref = child.getString("ref");
+            if (ref == null) {
+                continue;
+            }
+            JsonObject refNode = (JsonObject) yamlNodes.get(ref);
+            if (ref.equals(element.getName())
+                    || (refNode != null && element.getTitle() != null
+                            && 
element.getTitle().equals(refNode.getString("title")))) {
+                return child.getString("name");
+            }
+        }
+        return name;
+    }
+
+    /** The nodes of the YAML DSL model the catalog ships 
(schemas/camelYamlDsl-model.json), or null if it has none. */
+    private static JsonObject yamlDslNodes(CamelCatalog catalog) {
+        try (InputStream is = catalog.getVersionManager()
+                
.getResourceAsStream("org/apache/camel/catalog/schemas/camelYamlDsl-model.json"))
 {
+            if (is == null) {
+                return null;
+            }
+            JsonObject root = (JsonObject) Jsoner.deserialize(new 
String(is.readAllBytes(), StandardCharsets.UTF_8));
+            return (JsonObject) root.get("nodes");
+        } catch (Exception e) {
+            return null;
         }
     }
 
@@ -1727,11 +1822,8 @@ public final class CatalogDocs {
             enums.addAll(opt.getEnums());
             o.put("enumValues", enums);
         }
-        if ("element".equals(opt.getKind()) && "object".equals(opt.getType()) 
&& opt.getOneOfs() != null
-                && !opt.getOneOfs().isEmpty() && 
!opt.getOneOfs().contains(opt.getName())) {
-            // a single element that is one of several kinds is written as 
that kind, not under its own name: the
-            // error handler is errorHandler: {deadLetterChannel: {...}}, 
never errorHandlerType: (CAMEL-25370). Not
-            // a list such as outputs (written under steps:), nor an element 
whose kind is its own name (when).
+        if (isWrittenAsKind(opt)) {
+            // written as one of its kinds (CAMEL-25370)
             JsonArray oneOf = new JsonArray();
             oneOf.addAll(opt.getOneOfs());
             o.put("oneOf", oneOf);
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 97e8f023eb79..ac6e1d0a3a3d 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
@@ -27,6 +27,7 @@ import org.apache.camel.catalog.DefaultCamelCatalog;
 import org.apache.camel.impl.DefaultCamelContext;
 import org.apache.camel.support.DefaultExchange;
 import org.apache.camel.support.ExchangeHelper;
+import org.apache.camel.tooling.model.EipModel;
 import org.apache.camel.util.json.JsonArray;
 import org.apache.camel.util.json.JsonObject;
 import org.apache.camel.util.json.Jsoner;
@@ -547,4 +548,45 @@ class CatalogDocsTest {
                 null);
         assertFalse(doc.containsKey("nestedOptions"), doc.toJson());
     }
+
+    /** CAMEL-25377: a nested option is named under the element's YAML key, 
not its catalog name. */
+    @Test
+    void theNestedOptionsAreUnderTheYamlKey() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        var doc = CatalogDocs.catalogDoc(catalog, "routeTemplate", null, 
"eip", "defaultValue", null, false, false, null);
+        JsonArray nested = (JsonArray) doc.get("nestedOptions");
+        assertTrue(nested != null && !nested.isEmpty(), doc.toJson());
+        JsonObject first = (JsonObject) nested.get(0);
+        assertEquals("parameters", first.getString("under"), first.toJson());
+        assertTrue(doc.getString("nestedHint").contains("parameters: 
[{defaultValue: ...}]"), doc.toJson());
+    }
+
+    /** CAMEL-25377: the error handler's options are found in its kinds, under 
the kind to write. */
+    @Test
+    void theNestedOptionsOfTheErrorHandlerAreInItsKinds() {
+        CamelCatalog catalog = new DefaultCamelCatalog();
+        var doc = CatalogDocs.catalogDoc(catalog, "errorHandler", null, "eip", 
"deadLetterUri", null, false, false, null);
+        JsonArray nested = (JsonArray) doc.get("nestedOptions");
+        assertTrue(nested != null && !nested.isEmpty(), doc.toJson());
+        JsonObject first = (JsonObject) nested.get(0);
+        assertEquals("deadLetterChannel", first.getString("under"), 
first.toJson());
+        assertEquals("deadLetterUri", first.getString("name"), first.toJson());
+        assertTrue(doc.getString("nestedHint").contains("deadLetterChannel: 
{deadLetterUri: ...}"), doc.toJson());
+
+        // an option that several kinds have is listed once
+        doc = CatalogDocs.catalogDoc(catalog, "errorHandler", null, "eip", 
"useOriginalMessage", null, false, false, null);
+        nested = (JsonArray) doc.get("nestedOptions");
+        assertEquals(1, nested.stream().map(JsonObject.class::cast)
+                .filter(o -> 
"useOriginalMessage".equals(o.getString("name"))).count(), doc.toJson());
+        first = (JsonObject) nested.get(0);
+        assertEquals("useOriginalMessage", first.getString("name"), 
first.toJson());
+        assertEquals("deadLetterChannel", first.getString("under"), 
first.toJson());
+        assertTrue(((JsonArray) 
first.get("alsoUnder")).contains("defaultErrorHandler"), first.toJson());
+    }
+
+    @Test
+    void yamlKeyFallsBackToTheCatalogName() {
+        EipModel element = new 
DefaultCamelCatalog().eipModel("templateParameter");
+        assertEquals("templateParameter", CatalogDocs.yamlKey(null, 
"routeTemplate", "templateParameter", element));
+    }
 }

Reply via email to