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 7fdeeca7ce30 CAMEL-26877: camel-jbang - AI tools: camel_catalog_doc of 
an EIP lists the exchange properties it sets (#29699)
7fdeeca7ce30 is described below

commit 7fdeeca7ce30a4083c8ea29b92f06dd394307885
Author: Claus Ibsen <[email protected]>
AuthorDate: Thu Nov 12 13:09:32 2026 +0100

    CAMEL-26877: camel-jbang - AI tools: camel_catalog_doc of an EIP lists the 
exchange properties it sets (#29699)
    
    camel_catalog_doc of an EIP now lists the exchange properties the EIP
    sets, with their type and description, as includeHeaders lists the
    headers of a component. They are few and short, so they come by default.
    optionsFilter matches their names and descriptions too, and includeDoc
    shows them in the page instead of the include line that renders them on
    the website. In r32 of the local-model benchmark no answer for split named
    CamelSplitSize, which a streaming split sets on its last message only.
    
    
    Claude-Session: https://claude.ai/code/session_016Y4dDi5iQNgtHrDdz2NpwF
    
    Co-authored-by: Claude Opus 5.5 <[email protected]>
---
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    | 74 ++++++++++++++++++++--
 .../jbang/core/commands/ai/CatalogDocsTest.java    | 60 ++++++++++++++++++
 2 files changed, 128 insertions(+), 6 deletions(-)

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 2849704bf3de..3b8c56ece7c5 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
@@ -129,9 +129,10 @@ public final class CatalogDocs {
      * @param  endpoint       an endpoint URI to validate instead of 
documenting an artifact
      * @param  kind           component, dataformat, language, eip, bean or 
api; null to detect
      * @param  optionsFilter  keyword to match in option names, descriptions 
and groups; also picks the simple functions
-     *                        and operators to list in full. When no option of 
a component matches, its message headers
-     *                        are searched; when no function of simple 
matches, the file: functions; when nothing
-     *                        matches, the answer says where an option or 
header of that name is
+     *                        and operators to list in full, and the exchange 
properties of an EIP (by name and
+     *                        description). When no option of a component 
matches, its message headers are searched;
+     *                        when no function of simple matches, the file: 
functions; when nothing matches, the answer
+     *                        says where an option or header of that name is
      * @param  includeOptions which options to list: {@code common} (the 
default; without the deprecated and advanced
      *                        ones), {@code required}, {@code all} or {@code 
false}; {@code true} is {@code common}. A
      *                        filter searches all options
@@ -2342,14 +2343,18 @@ public final class CatalogDocs {
         }
     }
 
-    /** Whether a filter matched nothing in the answer: no option, nested 
option, header, function or operator. */
+    /**
+     * Whether a filter matched nothing in the answer: no option, nested 
option, header, exchange property, function or
+     * operator.
+     */
     private static boolean matchedNothing(JsonObject answer) {
         return answer.getIntegerOrDefault("matchedOptions", -1) == 0
                 && answer.getIntegerOrDefault("omittedOptions", 0) == 0
                 && answer.getIntegerOrDefault("matchedHeaders", 0) == 0
                 && answer.getIntegerOrDefault("matchedFunctions", 0) == 0
                 && answer.getIntegerOrDefault("matchedOperators", 0) == 0
-                && !answer.containsKey("nestedOptions") && 
!answer.containsKey("mainOptions");
+                && !answer.containsKey("nestedOptions") && 
!answer.containsKey("mainOptions")
+                && !answer.containsKey("exchangeProperties");
     }
 
     /**
@@ -3051,12 +3056,69 @@ public final class CatalogDocs {
         if (scope != OptionScope.NONE) {
             putOptions(result, model.getOptions(), filter, scope);
         }
+        putExchangeProperties(result, model, filter);
         if (doc != null) {
-            result.put("doc", doc);
+            result.put("doc", withExchangeProperties(doc, model));
         }
         return result;
     }
 
+    /**
+     * The exchange properties of an EIP, such as CamelSplitSize of split, 
listed the way the message headers of a
+     * component are. A model asked for split in 3 of 7 runs of a step, also 
with optionsFilter=Camel and includeDoc,
+     * and no answer named CamelSplitSize, which a streaming split sets on its 
last message only (r32 of the local-model
+     * benchmark, route-batch-file step 3). They are few and short, so an EIP 
lists them by default. A filter matches
+     * their names and descriptions, as it does for the options.
+     */
+    private static void putExchangeProperties(JsonObject result, EipModel 
model, String filter) {
+        JsonArray properties = new JsonArray();
+        for (EipModel.EipOptionModel p : model.getExchangeProperties()) {
+            if (filter == null || 
p.getName().toLowerCase(Locale.ROOT).contains(filter)
+                    || (p.getDescription() != null && 
p.getDescription().toLowerCase(Locale.ROOT).contains(filter))) {
+                properties.add(exchangePropertyToJson(p));
+            }
+        }
+        if (!properties.isEmpty()) {
+            result.put("exchangeProperties", properties);
+        }
+    }
+
+    /** One exchange property of an EIP as the tools return it, like a header: 
its name, type and description. */
+    private static JsonObject exchangePropertyToJson(BaseOptionModel p) {
+        JsonObject jo = new JsonObject();
+        jo.put("name", p.getName());
+        if (p.getJavaType() != null) {
+            jo.put("javaType", p.getJavaType());
+        }
+        if (p.getDescription() != null) {
+            jo.put("description", p.getDescription());
+        }
+        return jo;
+    }
+
+    /** The line of an EIP page that the website renders as the table of its 
exchange properties. */
+    private static final String EXCHANGE_PROPERTIES_INCLUDE = 
"include::partial$eip-exchangeProperties.adoc[]";
+
+    /**
+     * The page of an EIP with its exchange properties where the website shows 
them. The page in the catalog has only
+     * the include line there, so includeDoc=true for split returned an 
Exchange properties section with nothing but
+     * that line in it (r32 of the local-model benchmark). An EIP without 
exchange properties says so, as its page on
+     * the website does.
+     */
+    private static String withExchangeProperties(String doc, EipModel model) {
+        if (!doc.contains(EXCHANGE_PROPERTIES_INCLUDE)) {
+            return doc;
+        }
+        List<String> lines = new ArrayList<>();
+        for (EipModel.EipOptionModel p : model.getExchangeProperties()) {
+            lines.add("* " + p.getName() + (p.getJavaType() != null ? " (" + 
p.getJavaType() + ")" : "")
+                      + (p.getDescription() != null ? ": " + 
p.getDescription() : ""));
+        }
+        String text = lines.isEmpty()
+                ? "The " + model.getTitle() + " EIP has no exchange 
properties." : String.join("\n", lines);
+        return doc.replace(EXCHANGE_PROPERTIES_INCLUDE, text);
+    }
+
     /**
      * Which options a doc lists: the common ones by default, so the answer 
for a component such as kafka stays readable
      * for a small model; the deprecated and advanced ones on request. A 
filter searches all options, as it names what
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 f3c600932d83..894002065520 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
@@ -495,6 +495,66 @@ class CatalogDocsTest {
         assertTrue(key.getString("description").contains("key"));
     }
 
+    /**
+     * CAMEL-26877: an EIP lists the exchange properties it sets, as a 
component lists its headers. No answer for split
+     * named CamelSplitSize, which a streaming split sets on its last message 
only (r32 of the local-model benchmark).
+     */
+    @Test
+    void anEipListsTheExchangePropertiesItSets() throws Exception {
+        JsonObject split = catalogDoc(Map.of("name", "split", "kind", "eip"));
+        JsonObject size = exchangeProperty(split, "CamelSplitSize");
+        assertEquals("int", size.getString("javaType"));
+        assertTrue(size.getString("description").contains("not applied for 
stream based splitting"), size.toJson());
+        // the name, type and description of a message header, nothing more
+        assertEquals(List.of("name", "javaType", "description"), 
List.copyOf(size.keySet()));
+        assertEquals("boolean", exchangeProperty(split, 
"CamelSplitComplete").getString("javaType"));
+        assertEquals("int", exchangeProperty(split, 
"CamelSplitIndex").getString("javaType"));
+
+        // a filter matches their names and descriptions, as it matches the 
options
+        JsonObject camel = catalogDoc(Map.of("name", "split", "kind", "eip", 
"optionsFilter", "Camel"));
+        
assertTrue(exchangePropertyNames(camel).containsAll(List.of("CamelSplitIndex", 
"CamelSplitComplete",
+                "CamelSplitSize")), camel.toJson());
+        JsonObject bySize = catalogDoc(Map.of("name", "split", "kind", "eip", 
"optionsFilter", "size"));
+        assertEquals(List.of("CamelSplitSize"), exchangePropertyNames(bySize));
+        assertEquals(0, bySize.getInteger("matchedOptions"));
+        // the filter matched something, so no closest options and no other 
place to look
+        assertNull(bySize.get("closestOptions"), bySize.toJson());
+        assertNull(bySize.get("elsewhere"), bySize.toJson());
+        // an option the filter matches leaves out the exchange properties it 
does not match
+        JsonObject streaming = catalogDoc(Map.of("name", "split", "kind", 
"eip", "optionsFilter", "streaming"));
+        assertNotNull(option(streaming, "streaming"));
+        assertNull(streaming.get("exchangeProperties"), streaming.toJson());
+
+        // an EIP without exchange properties is unchanged
+        assertNull(catalogDoc(Map.of("name", "filter", "kind", 
"eip")).get("exchangeProperties"));
+        assertNull(catalogDoc(Map.of("name", "choice", "kind", "eip", 
"optionsFilter", "Camel")).get("exchangeProperties"));
+    }
+
+    /** CAMEL-26877: the page of an EIP lists its exchange properties, not the 
include line that renders them. */
+    @Test
+    void theEipPageHasItsExchangeProperties() throws Exception {
+        String split = catalogDoc(Map.of("name", "split", "kind", "eip", 
"includeDoc", true)).getString("doc");
+        assertFalse(split.contains("eip-exchangeProperties"), "the include 
line is replaced");
+        assertTrue(split.contains("== Exchange properties"), "the section is 
kept");
+        assertTrue(split.contains("\n* CamelSplitSize (int): The total number 
of Exchanges that was split."),
+                "listed in the section");
+        String choice = catalogDoc(Map.of("name", "choice", "kind", "eip", 
"includeDoc", true)).getString("doc");
+        assertFalse(choice.contains("eip-exchangeProperties"), "the include 
line is replaced");
+        assertTrue(choice.contains("The Choice EIP has no exchange 
properties."), "as the website says");
+    }
+
+    private static JsonObject exchangeProperty(JsonObject doc, String name) {
+        return ((JsonArray) 
doc.get("exchangeProperties")).stream().map(JsonObject.class::cast)
+                .filter(p -> 
name.equals(p.getString("name"))).findFirst().orElseThrow(() -> new 
AssertionError(
+                        name + " not in " + doc.toJson()));
+    }
+
+    private static List<String> exchangePropertyNames(JsonObject doc) {
+        JsonArray properties = (JsonArray) doc.get("exchangeProperties");
+        assertNotNull(properties, doc.toJson());
+        return properties.stream().map(p -> ((JsonObject) 
p).getString("name")).toList();
+    }
+
     @Test
     void theCommonOptionsAreTheDefaultAndTheScopeWidensThem() throws Exception 
{
         // the answer for kafka stays readable: deprecated and advanced 
options only on request, said in the answer

Reply via email to