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

Reply via email to