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

Reply via email to