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));
+ }
}