You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@camel.apache.org by da...@apache.org on 2026/10/02 18:45:16 UTC
(camel) branch fix/CAMEL-25283 created (now dd098a3a6bdf)
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a change to branch fix/CAMEL-25283
in repository https://gitbox.apache.org/repos/asf/camel.git
at dd098a3a6bdf CAMEL-25283: camel-kamelet - document how to write a custom Kamelet, and let camel_catalog_doc return a component's sub-pages
This branch includes the following new commits:
new dd098a3a6bdf CAMEL-25283: camel-kamelet - document how to write a custom Kamelet, and let camel_catalog_doc return a component's sub-pages
The 1 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails. The revisions
listed as "add" were already present in the repository and have only
been added to this reference.
(camel) 01/01: CAMEL-25283: camel-kamelet - document how to write a custom Kamelet, and let camel_catalog_doc return a component's sub-pages
Posted by da...@apache.org.
This is an automated email from the ASF dual-hosted git repository.
davsclaus pushed a commit to branch fix/CAMEL-25283
in repository https://gitbox.apache.org/repos/asf/camel.git
commit dd098a3a6bdf916dca467d635f35f012aaa95ead
Author: Claus Ibsen <cl...@gmail.com>
AuthorDate: Fri Oct 2 20:45:03 2026 +0200
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) <no...@anthropic.com>
Signed-off-by: Claus Ibsen <cl...@gmail.com>
---
.../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..3447a0499ffc
--- /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": "someone@example.com", "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..3447a0499ffc
--- /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": "someone@example.com", "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 2d1ff2dd2d84..f2bc35379490 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
@@ -473,6 +473,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');
}
@@ -499,6 +510,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);
@@ -919,6 +931,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"));
+ }
+}