This is an automated email from the ASF dual-hosted git repository.
Croway 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 5e0cea4fbc0a CAMEL-25539: camel-jbang - camel_catalog_doc says where
an EIP goes and the YAML shape of expression and namespace options (#27815)
5e0cea4fbc0a is described below
commit 5e0cea4fbc0a10c6434442e10246b884863f6e31
Author: Federico Mariani <[email protected]>
AuthorDate: Wed Oct 14 14:10:27 2026 +0200
CAMEL-25539: camel-jbang - camel_catalog_doc says where an EIP goes and the
YAML shape of expression and namespace options (#27815)
* CAMEL-25539: camel-jbang - camel_catalog_doc says where an EIP goes and
the YAML shape of expression and namespace options
Co-Authored-By: Claude Opus 5.5 <[email protected]>
* CAMEL-25539: the shape hints use the canonical YAML form and are checked
against the canonical schema; sampleHint only when a sample exists
Co-Authored-By: Claude Opus 5.5 <[email protected]>
---------
Co-authored-by: Claude Opus 5.5 <[email protected]>
---
.../dsl/jbang/core/commands/ai/CatalogDocs.java | 55 +++++++++++
.../dsl/jbang/core/commands/ai/CatalogSamples.java | 32 ++++++
.../jbang/core/commands/ai/CatalogDocsTest.java | 110 +++++++++++++++++++++
3 files changed, 197 insertions(+)
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 bebdb0eb51f8..0946496b39f6 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
@@ -119,6 +119,15 @@ public final class CatalogDocs {
*/
static final int DOC_EXCERPT_BUDGET_PROSE = 3600;
+ /** The java type of an expression option of an EIP other than its main
expression: handled, completionPredicate. */
+ private static final String EXPRESSION_SUB_ELEMENT =
"org.apache.camel.model.ExpressionSubElementDefinition";
+
+ /**
+ * The java type of a list of key/value properties. Today only the
namespace option of xpath, xquery and xtokenize
+ * has it; any other option of that type gets the generic key/value form.
+ */
+ private static final String PROPERTY_LIST =
"java.util.List<org.apache.camel.model.PropertyDefinition>";
+
private static final Pattern XREF =
Pattern.compile("xref:[^\\[]*\\[([^]]*)]");
private static final Pattern INTERNAL_REF =
Pattern.compile("<<[^,>]*,([^>]*)>>");
@@ -443,6 +452,10 @@ public final class CatalogDocs {
String matchedTerm) {
String doc = includeDoc ? catalog.asciiDoc(model.getName() + "-eip") :
null;
JsonObject result = eipDoc(model, filter, scope, doc);
+ if (CatalogSamples.hasEipSample(catalog, model.getName())) {
+ // only when there is one, else an agent spends a call that
answers "no sample" (CAMEL-25539)
+ result.put("sampleHint", "camel_catalog_sample name=" +
model.getName() + " returns a validated YAML example");
+ }
// only when the filter matched none of the EIP's own options, also
none the scope left out
if (filter != null && scope != OptionScope.NONE &&
result.getIntegerOrDefault("matchedOptions", 0) == 0
&& result.getIntegerOrDefault("omittedOptions", 0) == 0) {
@@ -1702,6 +1715,11 @@ public final class CatalogDocs {
result.put("input", model.isInput());
result.put("output", model.isOutput());
addCommonModelFields(result, model);
+ // where it goes: onException written as a step inside a route fails
the schema (CAMEL-25539)
+ String placement = CatalogSamples.placement(model);
+ if (placement != null) {
+ result.put("placement", placement);
+ }
if (scope != OptionScope.NONE) {
putOptions(result, model.getOptions(), filter, scope);
}
@@ -1829,7 +1847,44 @@ public final class CatalogDocs {
o.put("oneOf", oneOf);
o.put("yaml", "write one of oneOf as the key, not '" +
opt.getName() + "': for example "
+ opt.getOneOfs().get(0) + ": {...}");
+ } else {
+ String shape = yamlShape(opt);
+ if (shape != null) {
+ o.put("yaml", shape);
+ }
}
return o;
}
+
+ /**
+ * How an option whose type says only object or array is written in YAML
(CAMEL-25539): an expression has its
+ * language as the key (handled: true fails the schema), and a list of
properties is a list of key/value pairs
+ * (namespace of xpath). The example comes first in the canonical YAML
form, the explanation follows as a comment.
+ * Null for any other option.
+ */
+ static String yamlShape(BaseOptionModel opt) {
+ String name = opt.getName();
+ if ("expression".equals(opt.getKind()) &&
EXPRESSION_SUB_ELEMENT.equals(opt.getJavaType())) {
+ if (opt.isAsPredicate()) {
+ return name + ": {constant: {expression: \"true\"}} # a
predicate with its language as the key, not"
+ + " a plain true/false; simple: instead of constant:
for a condition";
+ }
+ return name + ": {header: {expression: myKey}} # an expression
with its language as the key (header,"
+ + " simple, jq, ...)";
+ }
+ if ("element".equals(opt.getKind()) && "object".equals(opt.getType())
&& opt.isAsPredicate()
+ && (opt.getOneOfs() == null || opt.getOneOfs().isEmpty())) {
+ return name + ": {expression: {simple: {expression:
\"${header.foo} == 'bar'\"}}} # a predicate under"
+ + " expression:, with its language as the key (simple, jq,
xpath, ...)";
+ }
+ if ("array".equals(opt.getType()) &&
PROPERTY_LIST.equals(opt.getJavaType())) {
+ String desc = opt.getDescription() != null ?
opt.getDescription().toLowerCase(Locale.ROOT) : "";
+ if (desc.contains("namespace")) {
+ return name + ": [{key: c, value: \"http://acme.com/cheese\"}]
# a list of key/value pairs: the"
+ + " prefix as key, the namespace uri as value";
+ }
+ return name + ": [{key: foo, value: bar}] # a list of key/value
pairs";
+ }
+ return null;
+ }
}
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
index af249d73d95d..b4a883b0872a 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogSamples.java
@@ -36,6 +36,7 @@ import java.util.stream.Collectors;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.tooling.model.ComponentModel;
import org.apache.camel.tooling.model.DataFormatModel;
+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;
@@ -742,6 +743,37 @@ public final class CatalogSamples {
return "a step inside the steps of a route (or of another EIP)";
}
+ /**
+ * Where an EIP of the catalog goes in a YAML file, for its
camel_catalog_doc answer (CAMEL-25539): a top-level
+ * entry, the error handler, a part of another EIP, or a step. Null for an
element written under the EIP or the
+ * configuration that holds it (resilience4jConfiguration, a load
balancer) and for a verb of rest, whose place the
+ * doc of that EIP gives.
+ */
+ static String placement(EipModel model) {
+ String name = model.getName();
+ if (ERROR_HANDLERS.contains(name) || TOP_LEVEL.contains(name)) {
+ return placement(name);
+ }
+ String partOf = PART_OF.get(name);
+ if (partOf != null) {
+ return "a part of " + partOf + ": written inside " + partOf + ",
not as a step of its own";
+ }
+ String label = model.getLabel() != null ? model.getLabel() : "";
+ if (model.isInput() && !List.of(label.split(",")).contains("rest")) {
+ return placement(name);
+ }
+ return null;
+ }
+
+ /**
+ * Whether camel_catalog_sample answers the EIP with at least one sample:
one validated from its documentation page,
+ * or a shipped one (also the sample of the EIP it is a part of, which
shows it in place).
+ */
+ static boolean hasEipSample(CamelCatalog catalog, String name) {
+ JsonArray samples = (JsonArray) sample(catalog, "eip", name,
1).get("samples");
+ return samples != null && !samples.isEmpty();
+ }
+
static String placement(CamelCatalog catalog, String kind, String name) {
switch (kind) {
case "component": {
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 ac6e1d0a3a3d..9e68769b1071 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
@@ -24,6 +24,7 @@ import java.util.Map;
import org.apache.camel.Exchange;
import org.apache.camel.catalog.CamelCatalog;
import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.dsl.yaml.validator.YamlValidator;
import org.apache.camel.impl.DefaultCamelContext;
import org.apache.camel.support.DefaultExchange;
import org.apache.camel.support.ExchangeHelper;
@@ -589,4 +590,113 @@ class CatalogDocsTest {
EipModel element = new
DefaultCamelCatalog().eipModel("templateParameter");
assertEquals("templateParameter", CatalogDocs.yamlKey(null,
"routeTemplate", "templateParameter", element));
}
+
+ /** CAMEL-25539: onException says it is a top-level entry, and handled
that it is an expression. */
+ @Test
+ void onExceptionSaysWhereItGoesAndTheShapeOfHandled() {
+ CamelCatalog catalog = new DefaultCamelCatalog();
+ var doc = CatalogDocs.catalogDoc(catalog, "onException", null, "eip",
null, "all", false, false, null);
+ assertTrue(doc.getString("placement").startsWith("top-level entry"),
doc.toJson());
+ assertTrue(doc.getString("sampleHint").contains("camel_catalog_sample
name=onException"), doc.toJson());
+ assertEquals("handled: {constant: {expression: \"true\"}}",
yamlExample(option(doc, "handled")));
+ assertEquals("continued: {constant: {expression: \"true\"}}",
yamlExample(option(doc, "continued")));
+ assertEquals("onWhen: {expression: {simple: {expression:
\"${header.foo} == 'bar'\"}}}",
+ yamlExample(option(doc, "onWhen")));
+ // the elements written in their own way keep no hint
+ assertFalse(option(doc, "redeliveryPolicy").containsKey("yaml"),
doc.toJson());
+ assertFalse(option(doc, "exception").containsKey("yaml"),
doc.toJson());
+ }
+
+ /** CAMEL-25539: each kind of EIP says where it goes, and a step has no
shape hint it does not need. */
+ @Test
+ void placementByKindOfEip() {
+ CamelCatalog catalog = new DefaultCamelCatalog();
+ var doc = CatalogDocs.catalogDoc(catalog, "errorHandler", null, "eip",
null, null, false, false, null);
+ assertTrue(doc.getString("placement").startsWith("errorHandler: with
the kind"), doc.toJson());
+ doc = CatalogDocs.catalogDoc(catalog, "routeConfiguration", null,
"eip", null, null, false, false, null);
+ assertTrue(doc.getString("placement").startsWith("top-level entry"),
doc.toJson());
+ doc = CatalogDocs.catalogDoc(catalog, "when", null, "eip", null, null,
false, false, null);
+ assertTrue(doc.getString("placement").startsWith("a part of choice"),
doc.toJson());
+ doc = CatalogDocs.catalogDoc(catalog, "split", null, "eip", null,
"all", false, false, null);
+ assertTrue(doc.getString("placement").startsWith("a step inside the
steps of a route"), doc.toJson());
+ for (Object o : (JsonArray) doc.get("options")) {
+ assertFalse(((JsonObject) o).containsKey("yaml"), ((JsonObject)
o).toJson());
+ }
+ assertTrue(doc.getString("sampleHint").contains("camel_catalog_sample
name=split"), doc.toJson());
+ // the sample hint only for an EIP camel_catalog_sample has a sample of
+ doc = CatalogDocs.catalogDoc(catalog, "pausable", null, "eip", null,
null, false, false, null);
+ assertTrue(doc.getString("placement").startsWith("a step inside the
steps of a route"), doc.toJson());
+ assertFalse(doc.containsKey("sampleHint"), doc.toJson());
+ }
+
+ /**
+ * CAMEL-25539: the shape hints show the expected YAML, which passes the
YAML DSL schema, the catalog checks and the
+ * canonical schema.
+ */
+ @Test
+ void theShapeHintsAreValidYaml() throws Exception {
+ CamelCatalog catalog = new DefaultCamelCatalog();
+ var onException = CatalogDocs.catalogDoc(catalog, "onException", null,
"eip", null, "all", false, false, null);
+ var aggregate = CatalogDocs.catalogDoc(catalog, "aggregate", null,
"eip", null, "all", false, false, null);
+ var xpath = CatalogDocs.catalogDoc(catalog, "xpath", null, "language",
null, null, false, false, null);
+ // the namespace of xpath, xquery and xtokenize is a list of key/value
pairs
+ assertEquals("namespace: [{key: c, value:
\"http://acme.com/cheese\"}]", yamlExample(option(xpath, "namespace")));
+ for (String language : List.of("xquery", "xtokenize")) {
+ var doc = CatalogDocs.catalogDoc(catalog, language, null,
"language", null, "all", false, false, null);
+ assertTrue(yamlExample(option(doc,
"namespace")).startsWith("namespace: [{key: c, "), doc.toJson());
+ }
+ String yaml = """
+ - onException:
+ exception: [java.io.IOException]
+ %s
+ %s
+ %s
+ steps:
+ - log:
+ message: failed
+ - onException:
+ exception: [java.net.ConnectException]
+ %s
+ - route:
+ from:
+ uri: direct:start
+ steps:
+ - setHeader:
+ name: price
+ expression:
+ xpath:
+ expression: /c:order/c:price/text()
+ %s
+ - aggregate:
+ aggregationStrategy:
"#class:org.apache.camel.processor.aggregate.GroupedBodyAggregationStrategy"
+ %s
+ %s
+ steps:
+ - log:
+ message: done
+ """.formatted(
+ yamlExample(option(onException, "handled")),
yamlExample(option(onException, "onWhen")),
+ yamlExample(option(onException, "retryWhile")),
yamlExample(option(onException, "continued")),
+ yamlExample(option(xpath, "namespace")),
yamlExample(option(aggregate, "correlationExpression")),
+ yamlExample(option(aggregate, "completionPredicate")));
+ assertEquals(List.of(), SourceValidator.validateCamelYaml(yaml,
catalog), yaml);
+ // and the canonical schema, which rejects the classic shorthand
(handled: {constant: "true"})
+ YamlValidator canonical = new YamlValidator(true);
+ canonical.init();
+ assertEquals(List.of(), YamlValidator.describeAll(yaml,
canonical.validate(yaml)), yaml);
+ }
+
+ private static JsonObject option(JsonObject doc, String name) {
+ return ((JsonArray)
doc.get("options")).stream().map(JsonObject.class::cast)
+ .filter(o ->
name.equals(o.getString("name"))).findFirst().orElseThrow(() -> new
AssertionError(
+ name + " not in " + doc.toJson()));
+ }
+
+ /** The YAML of a shape hint, without the comment that explains it. */
+ private static String yamlExample(JsonObject option) {
+ String yaml = option.getString("yaml");
+ assertNotNull(yaml, option.toJson());
+ int comment = yaml.indexOf(" # ");
+ return comment > 0 ? yaml.substring(0, comment) : yaml;
+ }
}