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