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 5e50108f3e3f CAMEL-23688: camel-jbang - Tests for camel catalog
kamelet (#27977)
5e50108f3e3f is described below
commit 5e50108f3e3f3e05482a14b2ccf8c335af0e14c3
Author: Claus Ibsen <[email protected]>
AuthorDate: Fri Oct 16 23:49:55 2026 +0200
CAMEL-23688: camel-jbang - Tests for camel catalog kamelet (#27977)
Tests for the listing of camel catalog kamelet (19% covered: only its
sort was tested, as the command downloads the catalog), with a few
Kamelets instead of the catalog: the table, --type, --filter by name or
description, JSON, and a catalog that cannot be downloaded. They found
these bugs, fixed here:
* --filter failed with a NullPointerException (exit 70) when a Kamelet
of the catalog has no description.
* An unknown --type showed nothing; now a usage error.
* A --filter that matched nothing printed nothing; it says so on stderr,
as the other catalog commands do.
The command also has --json now, as the other catalog commands.
Co-authored-by: Claude Opus 5.5 (1M context) <[email protected]>
---
.../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc | 6 +
.../camel-jbang-catalog-kamelet.adoc | 1 +
.../META-INF/camel-jbang-commands-metadata.json | 2 +-
.../core/commands/catalog/CatalogKamelet.java | 66 +++++++--
.../commands/catalog/CatalogKameletListTest.java | 159 +++++++++++++++++++++
5 files changed, 218 insertions(+), 16 deletions(-)
diff --git
a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
index cf02b0eaf395..0b85ee30f7fe 100644
--- a/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
+++ b/docs/user-manual/modules/ROOT/pages/camel-4x-upgrade-guide-4_24.adoc
@@ -558,6 +558,12 @@ The commands that sort their rows by `--sort`, such as
`camel ps`, `camel get ro
`camel catalog component`, exit with `2` and list their keys when the key is
unknown: `camel ps --sort=nmae` printed
the integrations unsorted. A row without the value of the key now comes first,
instead of failing the command.
+=== camel-jbang - camel catalog kamelet
+
+`camel catalog kamelet --filter` no longer fails with an internal error (`70`)
when a Kamelet has no description. An
+unknown `--type` exits with `2` (it showed nothing), a `--filter` that matches
nothing says so on stderr, and `--json`
+prints the Kamelets as JSON, as the other `camel catalog` commands do.
+
=== camel-jbang - camel get history
* `camel get history --diagram` and `--it` exit with `2` when several
integrations have a message history, and ask for
diff --git
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
index fd8a4208c305..84bc2aaf8628 100644
---
a/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
+++
b/docs/user-manual/modules/ROOT/pages/jbang-commands/camel-jbang-catalog-kamelet.adoc
@@ -22,6 +22,7 @@ camel catalog kamelet [options]
| `--download` | Whether to allow automatic downloading JAR dependencies (over
the internet) | true | boolean
| `--filter` | Filter by name or description | | String
| `--fresh` | Make sure we use fresh (i.e. non-cached) resources | false |
boolean
+| `--json` | Output in JSON Format | | boolean
| `--kamelets-version` | Apache Camel Kamelets version (auto-detected from
classpath if not set) | | String
| `--repo,--repos` | Additional maven repositories for download on-demand (Use
commas to separate multiple repositories) | | String
| `--sort` | Sort by name, type, support-level, or description | name | String
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
index dc38a95d8ecf..ee4d49be23bb 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
+++
b/dsl/camel-jbang/camel-jbang-core/src/generated/resources/META-INF/camel-jbang-commands-metadata.json
@@ -2,7 +2,7 @@
"commands": [
{ "name": "ask", "fullName": "ask", "description": "Ask a question about a
running Camel application using AI", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Ask", "options": [ { "names":
"--api-key", "description": "API key. Also reads ANTHROPIC_API_KEY,
OPENAI_API_KEY, WATSONX_APIKEY, or LLM_API_KEY env vars", "javaType":
"java.lang.String", "type": "string" }, { "names": "--api-type", "description":
"API type: 'ollama', 'openai', 'anthropic', or 'watsonx'", "javaType": [...]
{ "name": "bind", "fullName": "bind", "description": "DEPRECATED: Bind
source and sink Kamelets as a new Camel integration", "deprecated": true,
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.bind.Bind", "options":
[ { "names": "--error-handler", "description": "Add error handler
(none|log|sink:<endpoint>). Sink endpoints are expected in the format
[[apigroup\/]version:]kind:[namespace\/]name, plain Camel URIs or Kamelet
name.", "javaType": "java.lang.String", "type": "stri [...]
- { "name": "catalog", "fullName": "catalog", "description": "List artifacts
from Camel Catalog", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"component", "fullName": "catalog component", "description": "List components
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
+ { "name": "catalog", "fullName": "catalog", "description": "List artifacts
from Camel Catalog", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.catalog.CatalogCommand", "options": [
{ "names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"component", "fullName": "catalog component", "description": "List components
from the Camel Catalog", "sourceClass": "org.apache.camel.dsl.jbang.co [...]
{ "name": "cmd", "fullName": "cmd", "description": "Performs commands in
the running Camel integrations, such as start\/stop route, or change logging
levels.", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.action.CamelAction", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name":
"browse", "fullName": "cmd browse", "description": "Browse pending messages on
endpoints [...]
{ "name": "completion", "fullName": "completion", "description": "Generate
completion script for bash\/zsh", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.Complete", "options": [ { "names":
"-h,--help", "description": "Display the help and sub-commands", "javaType":
"boolean", "type": "boolean" } ] },
{ "name": "config", "fullName": "config", "description": "Get and set user
configuration values", "sourceClass":
"org.apache.camel.dsl.jbang.core.commands.config.ConfigCommand", "options": [ {
"names": "-h,--help", "description": "Display the help and sub-commands",
"javaType": "boolean", "type": "boolean" } ], "subcommands": [ { "name": "get",
"fullName": "config get", "description": "Display user configuration value",
"sourceClass": "org.apache.camel.dsl.jbang.core.commands.config. [...]
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
index ffb2111cf25f..570f6f769580 100644
---
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
+++
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKamelet.java
@@ -31,10 +31,13 @@ import
org.apache.camel.dsl.jbang.core.commands.CamelCommand;
import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
import org.apache.camel.dsl.jbang.core.commands.MavenResolverMixin;
import org.apache.camel.dsl.jbang.core.common.CamelTableColumns;
+import org.apache.camel.dsl.jbang.core.common.CliException;
import org.apache.camel.dsl.jbang.core.common.RowSort;
import org.apache.camel.dsl.jbang.core.common.TerminalWidthHelper;
import org.apache.camel.dsl.jbang.core.common.VersionHelper;
import org.apache.camel.util.StringHelper;
+import org.apache.camel.util.json.JsonObject;
+import org.apache.camel.util.json.Jsoner;
import picocli.CommandLine;
@CommandLine.Command(name = "kamelet",
@@ -46,6 +49,8 @@ import picocli.CommandLine;
" camel catalog kamelet --filter=kafka" })
public class CatalogKamelet extends CamelCommand {
+ private static final List<String> TYPES = List.of("source", "sink",
"action");
+
private static final RowSort<KameletModel> ORDER = new
RowSort<KameletModel>()
.text("name", r -> r.name)
.text("type", r -> r.type)
@@ -61,6 +66,10 @@ public class CatalogKamelet extends CamelCommand {
description = "Filter by type: source, sink, or
action")
String filterType;
+ @CommandLine.Option(names = { "--json" },
+ description = "Output in JSON Format")
+ boolean jsonOutput;
+
@CommandLine.Option(names = { "--filter" },
description = "Filter by name or description")
String filterName;
@@ -78,39 +87,40 @@ public class CatalogKamelet extends CamelCommand {
@Override
public Integer doCall() throws Exception {
- List<KameletModel> rows = new ArrayList<>();
+ // the options are wrong whatever the catalog has, so before loading it
+ Comparator<KameletModel> order = order();
+ if (filterType != null &&
!TYPES.contains(filterType.toLowerCase(Locale.ROOT))) {
+ throw CliException.usage("--type must be one of " + String.join(",
", TYPES) + ", was: " + filterType);
+ }
if (kameletsVersion == null) {
kameletsVersion = VersionHelper.extractKameletsVersion();
}
- Map<String, Object> kamelets;
+ List<KameletModel> rows;
try {
- kamelets = KameletCatalogHelper.loadKamelets(kameletsVersion,
mavenResolver.repos());
+ rows = new ArrayList<>(loadKamelets());
} catch (Exception e) {
printer().printErr("Cannot download camel-kamelets-catalog due to
" + e.getMessage());
return 1;
}
- for (Object o : kamelets.values()) {
- KameletModel row = KameletCatalogHelper.createModel(o, false);
- rows.add(row);
- }
-
if (filterType != null) {
- rows = rows.stream().filter(r ->
r.type.equalsIgnoreCase(filterType)).collect(Collectors.toList());
+ rows.removeIf(r -> !filterType.equalsIgnoreCase(r.type));
}
if (filterName != null) {
- filterName = filterName.toLowerCase(Locale.ROOT);
- rows = rows.stream().filter(
- r -> r.name.equalsIgnoreCase(filterName) ||
r.description.toLowerCase(Locale.ROOT).contains(filterName))
- .collect(Collectors.toList());
+ String text = filterName.toLowerCase(Locale.ROOT);
+ // a Kamelet may have no description
+ rows.removeIf(r -> !text.equalsIgnoreCase(r.name)
+ && (r.description == null ||
!r.description.toLowerCase(Locale.ROOT).contains(text)));
}
// sort rows
- rows.sort(order());
+ rows.sort(order);
- if (!rows.isEmpty()) {
+ if (jsonOutput) {
+
printer().println(Jsoner.serialize(rows.stream().map(CatalogKamelet::toJson).collect(Collectors.toList())));
+ } else if (!rows.isEmpty()) {
// Size the DESCRIPTION column to fill the terminal from the
measured width of the other columns.
int nameW = CamelTableColumns.measure("NAME",
CamelTableColumns.NAME_MAX, rows, r -> r.name);
int typeW = Math.max(10, CamelTableColumns.measure("TYPE",
Integer.MAX_VALUE, rows, r -> r.type));
@@ -124,9 +134,35 @@ public class CatalogKamelet extends CamelCommand {
CamelTableColumns.lastText("DESCRIPTION",
descWidth).with(this::getDescription))));
}
+ if (rows.isEmpty() && filterName != null) {
+ // not part of the output, which can be JSON
+ printer().printDiagnostic("No results for filter: " + filterName);
+ }
+
return 0;
}
+ /**
+ * The Kamelets of the catalog of the Kamelets version.
+ */
+ protected List<KameletModel> loadKamelets() throws Exception {
+ Map<String, Object> kamelets =
KameletCatalogHelper.loadKamelets(kameletsVersion, mavenResolver.repos());
+ List<KameletModel> models = new ArrayList<>();
+ for (Object o : kamelets.values()) {
+ models.add(KameletCatalogHelper.createModel(o, false));
+ }
+ return models;
+ }
+
+ private static JsonObject toJson(KameletModel r) {
+ JsonObject jo = new JsonObject();
+ jo.put("name", r.name);
+ jo.put("type", r.type);
+ jo.put("level", r.supportLevel);
+ jo.put("description", r.description);
+ return jo;
+ }
+
/**
* The order of the Kamelets by the --sort option.
*/
diff --git
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletListTest.java
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletListTest.java
new file mode 100644
index 000000000000..0470ab5df7f1
--- /dev/null
+++
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/catalog/CatalogKameletListTest.java
@@ -0,0 +1,159 @@
+/*
+ * 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.catalog;
+
+import java.io.IOException;
+import java.util.List;
+
+import org.apache.camel.dsl.jbang.core.commands.CamelCommandBaseTestSupport;
+import org.apache.camel.dsl.jbang.core.commands.CamelJBangMain;
+import org.apache.camel.dsl.jbang.core.common.CliException;
+import org.apache.camel.dsl.jbang.core.common.CliExitCode;
+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 picocli.CommandLine;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
+
+/**
+ * camel catalog kamelet: the Kamelets of the catalog (here a few, instead of
the download of the catalog), filtered by
+ * type, name or description, as a table or JSON.
+ */
+class CatalogKameletListTest extends CamelCommandBaseTestSupport {
+
+ @Test
+ void theKameletsByName() throws Exception {
+ assertThat(command().doCall()).isZero();
+
+ String[] lines = printer.getOutput().split(System.lineSeparator());
+ assertThat(lines[0]).contains("NAME", "TYPE", "LEVEL", "DESCRIPTION");
+ assertThat(lines).hasSize(5);
+ // the first sentence of the description
+ assertThat(lines[1]).contains("aws-s3-sink", "sink", "Preview",
"Writes to S3").doesNotContain("bucket");
+ assertThat(lines[2]).contains("kafka-source", "source", "Stable",
"Reads from Kafka");
+ assertThat(lines[3]).contains("log-action", "action");
+ assertThat(lines[4]).contains("timer-source", "source");
+ }
+
+ @Test
+ void filterByType() throws Exception {
+ assertThat(command("--type=SOURCE").doCall()).isZero();
+
+ assertThat(printer.getOutput()).contains("kafka-source",
"timer-source").doesNotContain("aws-s3-sink",
+ "log-action");
+ }
+
+ @Test
+ void unknownTypeIsAUsageError() {
+ CatalogKamelet command = command("--type=sources");
+
+ assertThatThrownBy(command::doCall)
+ .isInstanceOfSatisfying(CliException.class,
+ e ->
assertThat(e.getExitCode()).isEqualTo(CliExitCode.USAGE))
+ .hasMessage("--type must be one of source, sink, action, was:
sources");
+ }
+
+ @Test
+ void filterByNameOrDescription() throws Exception {
+ // the log-action Kamelet has no description
+ assertThat(command("--filter=kafka").doCall()).isZero();
+
+
assertThat(printer.getOutput()).contains("kafka-source").doesNotContain("timer-source",
"log-action");
+ }
+
+ @Test
+ void filterByTheWholeName() throws Exception {
+ assertThat(command("--filter=Log-Action").doCall()).isZero();
+
+
assertThat(printer.getOutput()).contains("log-action").doesNotContain("kafka-source");
+ }
+
+ @Test
+ void noResultsForTheFilter() throws Exception {
+ assertThat(command("--filter=mqtt").doCall()).isZero();
+
+ assertThat(printer.getStdout()).isEmpty();
+ assertThat(printer.getStderr()).isEqualTo("No results for filter:
mqtt");
+ }
+
+ @Test
+ void json() throws Exception {
+ assertThat(command("--json", "--type=sink").doCall()).isZero();
+
+ JsonArray kamelets = Jsoner.deserialize(printer.getStdout(),
(JsonArray) null);
+ assertThat(kamelets).hasSize(1);
+ JsonObject s3 = kamelets.getMap(0);
+ assertThat(s3.getString("name")).isEqualTo("aws-s3-sink");
+ assertThat(s3.getString("type")).isEqualTo("sink");
+ assertThat(s3.getString("level")).isEqualTo("Preview");
+ // the whole description
+ assertThat(s3.getString("description")).isEqualTo("Writes to S3. Each
message is a file in the bucket.");
+ }
+
+ @Test
+ void jsonWithoutResultsIsAnEmptyArray() throws Exception {
+ assertThat(command("--json", "--filter=mqtt").doCall()).isZero();
+
+ assertThat(printer.getStdout()).isEqualTo("[]");
+ assertThat(printer.getStderr()).isEqualTo("No results for filter:
mqtt");
+ }
+
+ @Test
+ void aCatalogThatCannotBeDownloadedIsAFailure() throws Exception {
+ CatalogKamelet command = new CatalogKamelet(new
CamelJBangMain().withPrinter(printer)) {
+ @Override
+ protected List<KameletModel> loadKamelets() throws Exception {
+ throw new IOException("no network");
+ }
+ };
+ CommandLine.populateCommand(command, "--kamelets-version=4.22.1");
+
+ assertThat(command.doCall()).isEqualTo(CliExitCode.FAILURE);
+
+ assertThat(printer.getStderr()).isEqualTo("ERROR: Cannot download
camel-kamelets-catalog due to no network");
+ }
+
+ private CatalogKamelet command(String... args) {
+ CatalogKamelet command = new CatalogKamelet(new
CamelJBangMain().withPrinter(printer)) {
+ @Override
+ protected List<KameletModel> loadKamelets() {
+ return List.of(
+ model("timer-source", "source", "Stable", "Produces
periodic events"),
+ model("kafka-source", "source", "Stable", "Reads from
Kafka"),
+ model("aws-s3-sink", "sink", "Preview", "Writes to S3.
Each message is a file in the bucket."),
+ model("log-action", "action", "Stable", null));
+ }
+ };
+ String[] withVersion = new String[args.length + 1];
+ withVersion[0] = "--kamelets-version=4.22.1";
+ System.arraycopy(args, 0, withVersion, 1, args.length);
+ CommandLine.populateCommand(command, withVersion);
+ return command;
+ }
+
+ private static KameletModel model(String name, String type, String
supportLevel, String description) {
+ KameletModel m = new KameletModel();
+ m.name = name;
+ m.type = type;
+ m.supportLevel = supportLevel;
+ m.description = description;
+ return m;
+ }
+}