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 b223a80dd6e1 CAMEL-25283: camel-kamelet - document how to write a 
custom Kamelet, and let camel_catalog_doc return a component's sub-pages 
(#27299)
b223a80dd6e1 is described below

commit b223a80dd6e16bfb001fca4dd7869579bff4f660
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 2 21:47:55 2026 +0200

    CAMEL-25283: camel-kamelet - document how to write a custom Kamelet, and 
let camel_catalog_doc return a component's sub-pages (#27299)
    
    * CAMEL-25283: camel-kamelet - document how to write a custom Kamelet, and 
let camel_catalog_doc return a component's sub-pages
    
    The kamelet page gets a short section on writing a custom Kamelet and its 
three kinds (source, sink, action), and a
    new sub-page kamelet-custom.adoc shows the file, the parameters ({{name}}, 
{{?name}}, defaults), one example of each
    kind, how a route uses them, and where Camel finds a Kamelet. The examples 
were run with camel run.
    
    camel_catalog_doc now answers docPage for components too: the sub-pages a 
component page links to under others are
    listed as docPages with their titles, and docPage=<page> returns one. A 
model is pointed at the Kamelet page from the
    kamelet answer, from its samples, and when a .kamelet.yaml it validates or 
writes is invalid.
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    * CAMEL-25283: write the inline placeholders as literal monospace so Antora 
does not read them as attributes
    
    Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
    
    ---------
    
    Signed-off-by: Claus Ibsen <[email protected]>
    Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
 .../org/apache/camel/catalog/docs.properties       |   1 +
 .../camel/catalog/docs/kamelet-component.adoc      |  12 ++
 .../apache/camel/catalog/docs/kamelet-custom.adoc  | 228 +++++++++++++++++++++
 .../src/main/docs/kamelet-component.adoc           |  12 ++
 .../src/main/docs/kamelet-custom.adoc              | 228 +++++++++++++++++++++
 docs/components/modules/others/nav.adoc            |   1 +
 .../modules/others/pages/kamelet-custom.adoc       |   1 +
 .../dsl/jbang/core/commands/ai/AuthoringTools.java |  13 ++
 .../dsl/jbang/core/commands/ai/CatalogDocs.java    |  48 ++++-
 .../dsl/jbang/core/commands/ai/CatalogSamples.java |   4 +
 .../commands/ai/CatalogDocComponentPagesTest.java  | 147 +++++++++++++
 11 files changed, 693 insertions(+), 2 deletions(-)

diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
index 2f4b8a857b91..3971d7d6053c 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs.properties
@@ -330,6 +330,7 @@ jta
 jte-component
 kafka-component
 kamelet-component
+kamelet-custom
 kamelet-eip
 kamelet-main
 kamelet-main-support
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-component.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-component.adoc
index 0ae87469eadf..dbf297fb88f6 100644
--- 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-component.adoc
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-component.adoc
@@ -39,6 +39,18 @@ The *kamelet* endpoint is *lenient*, which means that the 
endpoint accepts addit
 
 If a xref:manual::route-template.adoc[Route Template] is not found, the 
*kamelet* endpoint tries to load the related *kamelet* definition from the file 
system (by default `classpath:kamelets`). The default resolution mechanism 
expects _Kamelets_ files to have the extension `.kamelet.yaml`.
 
+=== Writing a custom Kamelet
+
+You can write your own Kamelets, for a piece of an integration that several 
routes need. A Kamelet is a
+`.kamelet.yaml` file with a route template and a description of its 
parameters, and it is one of three kinds,
+set by the `camel.apache.org/kamelet.type` label:
+
+* `source`: produces messages, and is used as the start of a route (`from: 
kamelet:<name>`).
+* `sink`: receives the message of a route and sends it somewhere (`to: 
kamelet:<name>`).
+* `action`: changes the message as a step in the middle of a route (`to: 
kamelet:<name>`).
+
+See xref:others:kamelet-custom.adoc[Writing a custom Kamelet] for the file, 
with an example of each kind.
+
 === Error Handling
 
 The error handling when using kamelets are using the same error handling
diff --git 
a/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-custom.adoc
 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-custom.adoc
new file mode 100644
index 000000000000..122e53d64257
--- /dev/null
+++ 
b/catalog/camel-catalog/src/generated/resources/org/apache/camel/catalog/docs/kamelet-custom.adoc
@@ -0,0 +1,228 @@
+= Kamelet - Writing a custom Kamelet
+:tabs-sync-option:
+
+xref:ROOT:kamelet-component.adoc[Back to Kamelet Component]
+
+A Kamelet is a route template in a `.kamelet.yaml` file, together with a 
description of the parameters it takes.
+You write one when the same piece of an integration is needed in several 
routes: a policy such as removing
+personal data, a connection to a system with your company's settings, or a 
destination every route writes to.
+The routes then use it by name, with their own parameter values, like any 
other endpoint.
+
+== The three kinds of Kamelet
+
+Every Kamelet is one of three kinds, set by the 
`camel.apache.org/kamelet.type` label. The kind decides where its
+template starts and ends, and where a route uses it.
+
+[cols="1,3,2",options="header"]
+|===
+| Kind | Template | Used in a route as
+
+| `source`
+| Starts at a component that produces messages (a timer, a queue, an API), and 
ends with `to: kamelet:sink`, which
+  hands each message to the route.
+| `from: kamelet:<name>`
+
+| `sink`
+| Starts with `from: kamelet:source`, which receives the message of the route, 
and ends at a component that sends
+  it somewhere (a file, a topic, an API).
+| `to: kamelet:<name>`, usually as the last step
+
+| `action`
+| Starts with `from: kamelet:source` and changes the message (filter fields, 
convert, enrich). The changed message
+  goes back to the route.
+| `to: kamelet:<name>`, as a step in the middle
+|===
+
+== The file
+
+A Kamelet file is a YAML object (not a list of routes like a `.camel.yaml` 
file) with these parts:
+
+`apiVersion` and `kind`:: always `camel.apache.org/v1` and `Kamelet`.
+`metadata.name`:: the name routes use in `kamelet:<name>`. Name the file after 
it: `<name>.kamelet.yaml`.
+`metadata.labels`:: `camel.apache.org/kamelet.type` with `source`, `sink` or 
`action`.
+`spec.definition`:: the parameters as a JSON schema: a `title` and 
`description` for the Kamelet, the names of the
+`required` parameters, and under `properties` each parameter with its `title`, 
`description`, `type`, and an
+optional `default`.
+`spec.dependencies`:: the Camel components and libraries the template uses, as 
`camel:<name>` or
+`mvn:<groupId>:<artifactId>:<version>`.
+`spec.template`:: the route itself, written as in the YAML DSL: a `from` with 
its `steps`, and optionally `beans`.
+
+In the template a parameter is written as a property placeholder: 
`+{{customer}}+`. A parameter that may be left out
+is written `+{{?customer}}+`, which removes the option it sets when no value 
is given, instead of failing.
+
+== A source Kamelet
+
+This source produces a sample order at a fixed interval. The route that uses 
it chooses the customer, and may change
+the interval, which has a default.
+
+.order-source.kamelet.yaml
+[source,yaml]
+----
+apiVersion: camel.apache.org/v1
+kind: Kamelet
+metadata:
+  name: order-source
+  labels:
+    camel.apache.org/kamelet.type: source
+spec:
+  definition:
+    title: Order Source
+    description: Produces a sample order at a fixed interval
+    required:
+      - customer
+    properties:
+      customer:
+        title: Customer
+        description: The customer name to put in the order
+        type: string
+      period:
+        title: Period
+        description: Milliseconds between orders
+        type: integer
+        default: 5000
+  dependencies:
+    - "camel:timer"
+  template:
+    from:
+      uri: timer:orders
+      parameters:
+        period: "{{period}}"
+      steps:
+        - setBody:
+            simple:
+              expression: '{"id": "${exchangeId}", "customer": "{{customer}}", 
"email": "[email protected]", "country": "DK"}'
+        - to:
+            uri: kamelet:sink
+----
+
+== An action Kamelet
+
+This action keeps only the fields of a JSON object that are named in its 
`allowlist` parameter, so personal data
+such as the customer and email does not travel further.
+
+.content-filter-action.kamelet.yaml
+[source,yaml]
+----
+apiVersion: camel.apache.org/v1
+kind: Kamelet
+metadata:
+  name: content-filter-action
+  labels:
+    camel.apache.org/kamelet.type: action
+spec:
+  definition:
+    title: Content Filter Action
+    description: Keeps only the listed fields of a JSON object
+    required:
+      - allowlist
+    properties:
+      allowlist:
+        title: Allowlist
+        description: Comma separated names of the fields to keep
+        type: string
+  dependencies:
+    - "camel:jq"
+  template:
+    from:
+      uri: kamelet:source
+      steps:
+        - setBody:
+            jq:
+              expression: 'with_entries(select(.key as $k | "{{allowlist}}" | 
split(",") | index($k)))'
+----
+
+== A sink Kamelet
+
+This sink writes each message to a file in a directory.
+
+.archive-sink.kamelet.yaml
+[source,yaml]
+----
+apiVersion: camel.apache.org/v1
+kind: Kamelet
+metadata:
+  name: archive-sink
+  labels:
+    camel.apache.org/kamelet.type: sink
+spec:
+  definition:
+    title: Archive Sink
+    description: Writes each message to a file in a directory
+    required:
+      - directory
+    properties:
+      directory:
+        title: Directory
+        description: The directory to write the files to
+        type: string
+      fileExtension:
+        title: File Extension
+        description: The extension of the file names
+        type: string
+        default: json
+  dependencies:
+    - "camel:file"
+  template:
+    from:
+      uri: kamelet:source
+      steps:
+        - to:
+            uri: "file:{{directory}}"
+            parameters:
+              fileName: "${exchangeId}.{{fileExtension}}"
+----
+
+== Using the Kamelets in a route
+
+A route uses each Kamelet by name, passing the parameters as endpoint 
parameters. Parameters with a default can be
+left out, or given to override the default:
+
+.orders.camel.yaml
+[source,yaml]
+----
+- route:
+    id: export-orders
+    from:
+      uri: kamelet:order-source
+      parameters:
+        customer: Donald
+        period: 2000
+      steps:
+        - to:
+            uri: kamelet:content-filter-action
+            parameters:
+              allowlist: id,country
+        - log:
+            message: "Filtered: ${body}"
+        - to:
+            uri: kamelet:archive-sink
+            parameters:
+              directory: target/orders
+----
+
+Each file written then holds only the fields that are allowed, such as 
`{"id":"...","country":"DK"}`.
+
+== Where Camel finds a Kamelet
+
+When a route uses `kamelet:<name>`, Camel loads `<name>.kamelet.yaml` from the 
`location` of the Kamelet component,
+which is `classpath:kamelets` by default. Set 
`camel.component.kamelet.location` to look elsewhere; several locations
+can be given, separated by comma.
+
+With the Camel CLI, the Kamelet files can simply be run with the route:
+
+[source,bash]
+----
+camel run orders.camel.yaml order-source.kamelet.yaml 
content-filter-action.kamelet.yaml archive-sink.kamelet.yaml
+----
+
+A Kamelet can also call another Kamelet in its template, as any route can.
+
+== Checking a Kamelet
+
+`camel validate yaml` checks a Kamelet file: the template against the YAML 
DSL, with the errors reported at their
+line in the template.
+
+[source,bash]
+----
+camel validate yaml content-filter-action.kamelet.yaml
+----
diff --git a/components/camel-kamelet/src/main/docs/kamelet-component.adoc 
b/components/camel-kamelet/src/main/docs/kamelet-component.adoc
index 0ae87469eadf..dbf297fb88f6 100644
--- a/components/camel-kamelet/src/main/docs/kamelet-component.adoc
+++ b/components/camel-kamelet/src/main/docs/kamelet-component.adoc
@@ -39,6 +39,18 @@ The *kamelet* endpoint is *lenient*, which means that the 
endpoint accepts addit
 
 If a xref:manual::route-template.adoc[Route Template] is not found, the 
*kamelet* endpoint tries to load the related *kamelet* definition from the file 
system (by default `classpath:kamelets`). The default resolution mechanism 
expects _Kamelets_ files to have the extension `.kamelet.yaml`.
 
+=== Writing a custom Kamelet
+
+You can write your own Kamelets, for a piece of an integration that several 
routes need. A Kamelet is a
+`.kamelet.yaml` file with a route template and a description of its 
parameters, and it is one of three kinds,
+set by the `camel.apache.org/kamelet.type` label:
+
+* `source`: produces messages, and is used as the start of a route (`from: 
kamelet:<name>`).
+* `sink`: receives the message of a route and sends it somewhere (`to: 
kamelet:<name>`).
+* `action`: changes the message as a step in the middle of a route (`to: 
kamelet:<name>`).
+
+See xref:others:kamelet-custom.adoc[Writing a custom Kamelet] for the file, 
with an example of each kind.
+
 === Error Handling
 
 The error handling when using kamelets are using the same error handling
diff --git a/components/camel-kamelet/src/main/docs/kamelet-custom.adoc 
b/components/camel-kamelet/src/main/docs/kamelet-custom.adoc
new file mode 100644
index 000000000000..122e53d64257
--- /dev/null
+++ b/components/camel-kamelet/src/main/docs/kamelet-custom.adoc
@@ -0,0 +1,228 @@
+= Kamelet - Writing a custom Kamelet
+:tabs-sync-option:
+
+xref:ROOT:kamelet-component.adoc[Back to Kamelet Component]
+
+A Kamelet is a route template in a `.kamelet.yaml` file, together with a 
description of the parameters it takes.
+You write one when the same piece of an integration is needed in several 
routes: a policy such as removing
+personal data, a connection to a system with your company's settings, or a 
destination every route writes to.
+The routes then use it by name, with their own parameter values, like any 
other endpoint.
+
+== The three kinds of Kamelet
+
+Every Kamelet is one of three kinds, set by the 
`camel.apache.org/kamelet.type` label. The kind decides where its
+template starts and ends, and where a route uses it.
+
+[cols="1,3,2",options="header"]
+|===
+| Kind | Template | Used in a route as
+
+| `source`
+| Starts at a component that produces messages (a timer, a queue, an API), and 
ends with `to: kamelet:sink`, which
+  hands each message to the route.
+| `from: kamelet:<name>`
+
+| `sink`
+| Starts with `from: kamelet:source`, which receives the message of the route, 
and ends at a component that sends
+  it somewhere (a file, a topic, an API).
+| `to: kamelet:<name>`, usually as the last step
+
+| `action`
+| Starts with `from: kamelet:source` and changes the message (filter fields, 
convert, enrich). The changed message
+  goes back to the route.
+| `to: kamelet:<name>`, as a step in the middle
+|===
+
+== The file
+
+A Kamelet file is a YAML object (not a list of routes like a `.camel.yaml` 
file) with these parts:
+
+`apiVersion` and `kind`:: always `camel.apache.org/v1` and `Kamelet`.
+`metadata.name`:: the name routes use in `kamelet:<name>`. Name the file after 
it: `<name>.kamelet.yaml`.
+`metadata.labels`:: `camel.apache.org/kamelet.type` with `source`, `sink` or 
`action`.
+`spec.definition`:: the parameters as a JSON schema: a `title` and 
`description` for the Kamelet, the names of the
+`required` parameters, and under `properties` each parameter with its `title`, 
`description`, `type`, and an
+optional `default`.
+`spec.dependencies`:: the Camel components and libraries the template uses, as 
`camel:<name>` or
+`mvn:<groupId>:<artifactId>:<version>`.
+`spec.template`:: the route itself, written as in the YAML DSL: a `from` with 
its `steps`, and optionally `beans`.
+
+In the template a parameter is written as a property placeholder: 
`+{{customer}}+`. A parameter that may be left out
+is written `+{{?customer}}+`, which removes the option it sets when no value 
is given, instead of failing.
+
+== A source Kamelet
+
+This source produces a sample order at a fixed interval. The route that uses 
it chooses the customer, and may change
+the interval, which has a default.
+
+.order-source.kamelet.yaml
+[source,yaml]
+----
+apiVersion: camel.apache.org/v1
+kind: Kamelet
+metadata:
+  name: order-source
+  labels:
+    camel.apache.org/kamelet.type: source
+spec:
+  definition:
+    title: Order Source
+    description: Produces a sample order at a fixed interval
+    required:
+      - customer
+    properties:
+      customer:
+        title: Customer
+        description: The customer name to put in the order
+        type: string
+      period:
+        title: Period
+        description: Milliseconds between orders
+        type: integer
+        default: 5000
+  dependencies:
+    - "camel:timer"
+  template:
+    from:
+      uri: timer:orders
+      parameters:
+        period: "{{period}}"
+      steps:
+        - setBody:
+            simple:
+              expression: '{"id": "${exchangeId}", "customer": "{{customer}}", 
"email": "[email protected]", "country": "DK"}'
+        - to:
+            uri: kamelet:sink
+----
+
+== An action Kamelet
+
+This action keeps only the fields of a JSON object that are named in its 
`allowlist` parameter, so personal data
+such as the customer and email does not travel further.
+
+.content-filter-action.kamelet.yaml
+[source,yaml]
+----
+apiVersion: camel.apache.org/v1
+kind: Kamelet
+metadata:
+  name: content-filter-action
+  labels:
+    camel.apache.org/kamelet.type: action
+spec:
+  definition:
+    title: Content Filter Action
+    description: Keeps only the listed fields of a JSON object
+    required:
+      - allowlist
+    properties:
+      allowlist:
+        title: Allowlist
+        description: Comma separated names of the fields to keep
+        type: string
+  dependencies:
+    - "camel:jq"
+  template:
+    from:
+      uri: kamelet:source
+      steps:
+        - setBody:
+            jq:
+              expression: 'with_entries(select(.key as $k | "{{allowlist}}" | 
split(",") | index($k)))'
+----
+
+== A sink Kamelet
+
+This sink writes each message to a file in a directory.
+
+.archive-sink.kamelet.yaml
+[source,yaml]
+----
+apiVersion: camel.apache.org/v1
+kind: Kamelet
+metadata:
+  name: archive-sink
+  labels:
+    camel.apache.org/kamelet.type: sink
+spec:
+  definition:
+    title: Archive Sink
+    description: Writes each message to a file in a directory
+    required:
+      - directory
+    properties:
+      directory:
+        title: Directory
+        description: The directory to write the files to
+        type: string
+      fileExtension:
+        title: File Extension
+        description: The extension of the file names
+        type: string
+        default: json
+  dependencies:
+    - "camel:file"
+  template:
+    from:
+      uri: kamelet:source
+      steps:
+        - to:
+            uri: "file:{{directory}}"
+            parameters:
+              fileName: "${exchangeId}.{{fileExtension}}"
+----
+
+== Using the Kamelets in a route
+
+A route uses each Kamelet by name, passing the parameters as endpoint 
parameters. Parameters with a default can be
+left out, or given to override the default:
+
+.orders.camel.yaml
+[source,yaml]
+----
+- route:
+    id: export-orders
+    from:
+      uri: kamelet:order-source
+      parameters:
+        customer: Donald
+        period: 2000
+      steps:
+        - to:
+            uri: kamelet:content-filter-action
+            parameters:
+              allowlist: id,country
+        - log:
+            message: "Filtered: ${body}"
+        - to:
+            uri: kamelet:archive-sink
+            parameters:
+              directory: target/orders
+----
+
+Each file written then holds only the fields that are allowed, such as 
`{"id":"...","country":"DK"}`.
+
+== Where Camel finds a Kamelet
+
+When a route uses `kamelet:<name>`, Camel loads `<name>.kamelet.yaml` from the 
`location` of the Kamelet component,
+which is `classpath:kamelets` by default. Set 
`camel.component.kamelet.location` to look elsewhere; several locations
+can be given, separated by comma.
+
+With the Camel CLI, the Kamelet files can simply be run with the route:
+
+[source,bash]
+----
+camel run orders.camel.yaml order-source.kamelet.yaml 
content-filter-action.kamelet.yaml archive-sink.kamelet.yaml
+----
+
+A Kamelet can also call another Kamelet in its template, as any route can.
+
+== Checking a Kamelet
+
+`camel validate yaml` checks a Kamelet file: the template against the YAML 
DSL, with the errors reported at their
+line in the template.
+
+[source,bash]
+----
+camel validate yaml content-filter-action.kamelet.yaml
+----
diff --git a/docs/components/modules/others/nav.adoc 
b/docs/components/modules/others/nav.adoc
index ebccf4f508eb..4a991e597a07 100644
--- a/docs/components/modules/others/nav.adoc
+++ b/docs/components/modules/others/nav.adoc
@@ -38,6 +38,7 @@
 ** xref:jfr.adoc[JFR]
 ** xref:jsoup.adoc[Jsoup]
 ** xref:jta.adoc[JTA]
+** xref:kamelet-custom.adoc[Kamelet - Writing a custom Kamelet]
 ** xref:keycloak-consumer.adoc[Keycloak Consumer Operations]
 ** xref:keycloak-producer.adoc[Keycloak Producer Operations]
 ** xref:keycloak-security.adoc[Keycloak Security Policies]
diff --git a/docs/components/modules/others/pages/kamelet-custom.adoc 
b/docs/components/modules/others/pages/kamelet-custom.adoc
new file mode 120000
index 000000000000..be528337c081
--- /dev/null
+++ b/docs/components/modules/others/pages/kamelet-custom.adoc
@@ -0,0 +1 @@
+../../../../../components/camel-kamelet/src/main/docs/kamelet-custom.adoc
\ No newline at end of file
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
index 216298f941e4..c529be10c390 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AuthoringTools.java
@@ -477,6 +477,17 @@ public final class AuthoringTools {
         return sb.toString();
     }
 
+    /** Where the shape of a Kamelet file is explained, said where an agent 
gets one wrong (CAMEL-25283). */
+    static final String KAMELET_GUIDE = "How to write a Kamelet (the file, and 
the source, sink and action kinds): "
+                                        + "camel_catalog_doc name=kamelet 
docPage=custom";
+
+    private static void putKameletGuide(JsonObject result, String file, 
List<String> errors) {
+        String name = file != null ? file.toLowerCase(Locale.ROOT) : "";
+        if (!errors.isEmpty() && (name.endsWith(".kamelet.yaml") || 
name.endsWith(".kamelet.yml"))) {
+            result.put("guide", KAMELET_GUIDE);
+        }
+    }
+
     private static String commaLines(String list) {
         return list == null ? null : list.replace(',', '\n');
     }
@@ -503,6 +514,7 @@ public final class AuthoringTools {
         result.put("valid", errors.isEmpty());
         result.put("file", file);
         result.put("errors", new JsonArray(errors));
+        putKameletGuide(result, file, errors);
         // the problems whose fix is certain, as edits an agent can apply 
(camel_edit_file find/replace)
         JsonArray fixes = new JsonArray();
         String[] lines = content.split("\n", -1);
@@ -923,6 +935,7 @@ public final class AuthoringTools {
                 result.put("errors", new JsonArray(errors));
                 result.put("message", "The file was not written: the content 
has validation errors. Fix them and"
                                       + " call camel_write_file again.");
+                putKameletGuide(result, file, errors);
                 return result;
             }
         }
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 8472684617c3..dd4b522109f0 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
@@ -19,10 +19,13 @@ package org.apache.camel.dsl.jbang.core.commands.ai;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Locale;
 import java.util.Map;
+import java.util.Set;
 import java.util.TreeMap;
+import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
 import org.apache.camel.catalog.CamelCatalog;
@@ -197,9 +200,21 @@ public final class CatalogDocs {
             ComponentModel cm = catalog.componentModel(name);
             if (cm != null) {
                 String adoc = catalog.asciiDoc(name + "-component");
+                List<JsonObject> pages = componentDocPages(cm.getScheme(), 
adoc);
+                if (page != null && !page.isEmpty()) {
+                    // a sub-page the component page links to, such as how to 
write a custom Kamelet
+                    String sub = pages.stream().anyMatch(p -> 
page.equals(p.getString("page")))
+                            ? catalog.asciiDoc(cm.getScheme() + "-" + page) : 
null;
+                    if (sub == null) {
+                        JsonObject err = error("No doc page '" + page + "' for 
component " + name);
+                        err.put("docPages", new JsonArray(pages));
+                        return err;
+                    }
+                    return componentDoc(cm, lowerFilter, OptionScope.NONE, 
false, sub, null, pages);
+                }
                 // the whole page when it was asked for, else its first 
section, which the options cannot say
                 return componentDoc(cm, lowerFilter, scope, includeHeaders, 
includeDoc ? adoc : null,
-                        includeDoc ? null : docExcerpt(adoc, 
DOC_EXCERPT_BUDGET));
+                        includeDoc ? null : docExcerpt(adoc, 
DOC_EXCERPT_BUDGET), pages);
             }
             JsonObject group = mainOptionsGroup(catalog, name);
             if (group != null) {
@@ -1032,7 +1047,7 @@ public final class CatalogDocs {
 
     private static JsonObject componentDoc(
             ComponentModel model, String filter, OptionScope scope, boolean 
includeHeaders, String doc,
-            String docExcerpt) {
+            String docExcerpt, List<JsonObject> docPages) {
         JsonObject result = new JsonObject();
         result.put("kind", "component");
         result.put("name", model.getScheme());
@@ -1114,9 +1129,38 @@ public final class CatalogDocs {
             result.put("documentation", docExcerpt);
             result.put("documentationHint", "the start of the component's 
documentation page; includeDoc=true for all of it");
         }
+        if (!docPages.isEmpty()) {
+            // named with their titles, so a model can tell which one answers 
its question
+            result.put("docPages", new JsonArray(docPages));
+            result.put("docPagesHint", "docPage=<page> returns that 
documentation page as text");
+        }
         return result;
     }
 
+    /**
+     * The sub-pages of a component's documentation, found from the links of 
its page to them
+     * ({@code xref:others:kamelet-custom.adoc[Writing a custom Kamelet]}): 
the page name to ask for with
+     * {@code docPage}, and its title.
+     */
+    static List<JsonObject> componentDocPages(String scheme, String adoc) {
+        List<JsonObject> pages = new ArrayList<>();
+        if (adoc == null || scheme == null) {
+            return pages;
+        }
+        Matcher m = Pattern.compile("xref:others:" + Pattern.quote(scheme) + 
"-([a-z0-9][a-z0-9-]*)\\.adoc\\[([^\\]]+)]")
+                .matcher(adoc);
+        Set<String> seen = new HashSet<>();
+        while (m.find()) {
+            if (seen.add(m.group(1))) {
+                JsonObject jo = new JsonObject();
+                jo.put("page", m.group(1));
+                jo.put("title", m.group(2));
+                pages.add(jo);
+            }
+        }
+        return pages;
+    }
+
     private static JsonObject dataFormatDoc(
             DataFormatModel model, String filter, OptionScope scope, String 
doc, String docExcerpt) {
         JsonObject result = new JsonObject();
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 42f9856ca1e5..da2cdc36882f 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
@@ -533,6 +533,10 @@ public final class CatalogSamples {
             answer.put("note", "none of the examples of the documentation uses 
a " + name + " endpoint; they show the "
                                + "component in another way (a properties 
function, a policy, a converter)");
         }
+        if ("component".equals(kind) && "kamelet".equals(name)) {
+            // the samples show how a route uses a Kamelet; writing the 
.kamelet.yaml file is on its own page
+            answer.put("guide", AuthoringTools.KAMELET_GUIDE);
+        }
         return answer;
     }
 
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocComponentPagesTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocComponentPagesTest.java
new file mode 100644
index 000000000000..321036374ef9
--- /dev/null
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/CatalogDocComponentPagesTest.java
@@ -0,0 +1,147 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.camel.dsl.jbang.core.commands.ai;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import org.apache.camel.catalog.DefaultCamelCatalog;
+import org.apache.camel.util.json.JsonArray;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+/**
+ * CAMEL-25283: the sub-pages of a component's documentation can be asked for 
with {@code docPage}, and a model is told
+ * where the page on writing a custom Kamelet is: in the kamelet answer, in 
its samples, and when a Kamelet file it
+ * wrote is invalid.
+ */
+class CatalogDocComponentPagesTest {
+
+    private static JsonObject catalogDoc(Map<String, String> args) throws 
Exception {
+        String json = String.valueOf(ToolRegistry.execute("camel_catalog_doc", 
new ToolContext(), new HashMap<>(args)));
+        return (JsonObject) Jsoner.deserialize(json);
+    }
+
+    private static JsonObject page(JsonArray pages, String name) {
+        for (Object o : pages) {
+            JsonObject p = (JsonObject) o;
+            if (name.equals(p.getString("page"))) {
+                return p;
+            }
+        }
+        return null;
+    }
+
+    @Test
+    public void testTheKameletAnswerNamesTheCustomKameletPage() throws 
Exception {
+        JsonObject answer = catalogDoc(Map.of("name", "kamelet"));
+        JsonArray pages = (JsonArray) answer.get("docPages");
+        assertNotNull(pages, "no docPages in: " + answer.toJson());
+        JsonObject custom = page(pages, "custom");
+        assertNotNull(custom, "custom is not a doc page: " + pages.toJson());
+        assertEquals("Writing a custom Kamelet", custom.getString("title"));
+        assertNotNull(answer.getString("docPagesHint"));
+        // the pages of another artifact that share the prefix are not 
sub-pages of the component
+        assertNull(page(pages, "main"));
+    }
+
+    @Test
+    public void testTheCustomKameletPageIsReturnedAsText() throws Exception {
+        JsonObject answer = catalogDoc(Map.of("name", "kamelet", "docPage", 
"custom"));
+        String doc = answer.getString("doc");
+        assertNotNull(doc, "no doc in: " + answer.toJson());
+        for (String kind : List.of("source", "sink", "action")) {
+            assertTrue(doc.contains("camel.apache.org/kamelet.type: " + kind), 
"no " + kind + " Kamelet in the page");
+        }
+        assertTrue(doc.contains("{{?"), "the page does not say how an optional 
parameter is written");
+        // the page is the answer: no option list around it
+        assertNull(answer.get("options"));
+        assertNull(answer.get("documentation"));
+    }
+
+    @Test
+    public void testAnUnknownPageListsThePagesThereAre() throws Exception {
+        JsonObject answer = catalogDoc(Map.of("name", "kamelet", "docPage", 
"nosuchpage"));
+        assertNotNull(answer.getString("error"));
+        assertNotNull(page((JsonArray) answer.get("docPages"), "custom"));
+    }
+
+    @Test
+    public void testTheSubPagesOfOtherComponentsAreFoundToo() throws Exception 
{
+        JsonArray pages = (JsonArray) catalogDoc(Map.of("name", 
"aws2-s3")).get("docPages");
+        assertNotNull(pages);
+        assertNotNull(page(pages, "streaming"), pages.toJson());
+        assertNotNull(page(pages, "consumer-examples"), pages.toJson());
+        String doc = catalogDoc(Map.of("name", "aws2-s3", "docPage", 
"streaming")).getString("doc");
+        assertTrue(doc.startsWith("= AWS S3 - Streaming Upload"), 
doc.substring(0, Math.min(80, doc.length())));
+    }
+
+    @Test
+    public void testAComponentWithoutSubPagesHasNoDocPages() throws Exception {
+        assertNull(catalogDoc(Map.of("name", "timer")).get("docPages"));
+    }
+
+    @Test
+    public void testAnInvalidKameletFileSaysWhereTheGuideIs() {
+        String kamelet = """
+                apiVersion: camel.apache.org/v1
+                kind: Kamelet
+                metadata:
+                  name: my-action
+                  labels:
+                    camel.apache.org/kamelet.type: action
+                spec:
+                  definition:
+                    title: My Action
+                  template:
+                    from:
+                      uri: kamelet:source
+                      steps:
+                        - jq: "."
+                """;
+        JsonObject invalid = AuthoringTools.validate(new ToolContext(), 
"my-action.kamelet.yaml", kamelet);
+        assertFalse(invalid.getBoolean("valid"));
+        assertEquals(AuthoringTools.KAMELET_GUIDE, invalid.getString("guide"));
+
+        String fixed
+                = kamelet.replace("- jq: \".\"", "- setBody:\n                 
 jq:\n                    expression: \".\"");
+        JsonObject valid = AuthoringTools.validate(new ToolContext(), 
"my-action.kamelet.yaml", fixed);
+        assertTrue(valid.getBoolean("valid"), valid.toJson());
+        assertNull(valid.get("guide"));
+
+        // a route file is not a Kamelet
+        JsonObject route = AuthoringTools.validate(new ToolContext(), 
"my.camel.yaml",
+                "- from:\n    uri: timer:x\n    steps:\n      - jq: \".\"\n");
+        assertFalse(route.getBoolean("valid"));
+        assertNull(route.get("guide"));
+    }
+
+    @Test
+    public void testTheKameletSamplesSayWhereTheGuideIs() {
+        JsonObject answer = CatalogSamples.sample(new DefaultCamelCatalog(), 
"component", "kamelet", 2);
+        assertEquals(AuthoringTools.KAMELET_GUIDE, answer.getString("guide"), 
answer.toJson());
+        assertNull(CatalogSamples.sample(new DefaultCamelCatalog(), 
"component", "timer", 2).get("guide"));
+    }
+}

Reply via email to