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 2d3d14d9c108 CAMEL-26929: camel-jbang - camel cmd route-structure 
--json applies --filter, --sort, --brief and --description (#29934)
2d3d14d9c108 is described below

commit 2d3d14d9c10800d80b24770e35b63ebd0cbf0b2d
Author: Claus Ibsen <[email protected]>
AuthorDate: Sat Nov 14 19:28:45 2026 +0100

    CAMEL-26929: camel-jbang - camel cmd route-structure --json applies 
--filter, --sort, --brief and --description (#29934)
    
    They were for the text output only, and a filter that matched no route 
exited 0 with all the
    routes in the JSON. The routes are now picked, sorted and shortened once, 
for the text and the
    JSON, as camel cmd route-dump does.
    
    CAMEL-26929: camel-jbang - parentheses for the filter condition of 
route-structure
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
---
 .../ROOT/pages/camel-4x-upgrade-guide-4_24.adoc    |   3 +
 .../commands/action/CamelRouteStructureAction.java | 133 ++++++++++++---------
 .../action/CamelRouteStructureActionTest.java      | 110 +++++++++++++++++
 3 files changed, 191 insertions(+), 55 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 ddb0101eda9d..663441224063 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
@@ -2794,6 +2794,9 @@ A script that checks the exit code of a command must take 
these changes into acc
 * A source file of `camel cmd route-diagram`, `camel cmd route-structure` and 
`camel cmd route-topology` that does not
   exist exits with `66` instead of `1`. The parts of a Java route the parser 
could not read are printed as a warning on
   stderr (also with `--json`, which lists them in `unread` as before).
+* `camel cmd route-structure --json` has the routes of `--filter`, in the 
order of `--sort`, with the code of
+  `--brief` or `--description`, as the text output: these options were ignored 
with `--json`. A filter that matches no
+  route exits with `3` instead of `0` with all the routes.
 * A file or directory that does not exist exits with `66` instead of `1`: a 
source file or `--source-dir` of
   `camel run`, the route file of `camel explain` and `camel harden` (checked 
before the LLM is contacted), and the
   `pom.xml` of `camel dependency runtime`.
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
index af6353e04730..543cac05d4bd 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureAction.java
@@ -121,8 +121,6 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
 
     @Override
     public Integer doCall() throws Exception {
-        List<Row> rows = new ArrayList<>();
-
         JsonObject jo;
         List<CamelProcess> processes = ProcessFinder.find(name);
         if (processes.size() > 1) {
@@ -145,68 +143,91 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
         if (varied) {
             jo = onlyVaried(jo);
             JsonArray routes = jo.getCollection("routes");
-            if (!jsonOutput && (routes == null || routes.isEmpty())) {
-                printer().println("No step has been reached with more than one 
body type (the message history records"
-                                  + " the body types, such as with camel run 
--dev)");
+            if (routes == null || routes.isEmpty()) {
+                if (jsonOutput) {
+                    addUnreadSourceParts(jo);
+                    printer().println(Jsoner.prettyPrint(jo.toJson(), 2));
+                } else {
+                    printer().println("No step has been reached with more than 
one body type (the message history"
+                                      + " records the body types, such as with 
camel run --dev)");
+                }
                 return 0;
             }
         }
+
+        // the same routes for the JSON as for the text: --filter, --sort, 
--brief and --description were for the text
+        // only, and a filter that matched nothing exited 0 with all the 
routes in the JSON (CAMEL-26929)
+        List<Row> rows = toRows(jo);
+        rows.sort(ORDER.comparator(sort));
+        if (rows.isEmpty() && filter != null) {
+            throw CliException.notFound("No route matching: " + filter);
+        }
         if (jsonOutput) {
-            addUnreadSourceParts(jo);
-            String dump = Jsoner.prettyPrint(jo.toJson(), 2);
-            printer().println(dump);
-        } else {
-            JsonArray arr = (JsonArray) jo.get("routes");
-            for (int i = 0; i < arr.size(); i++) {
-                JsonObject o = (JsonObject) arr.get(i);
-                Row row = new Row();
-                row.location = extractSourceName(o.getString("source"));
-                row.routeId = o.getString("routeId");
-                row.checksum = o.getString("checksum");
-                if (!rows.contains(row)) {
-                    List<JsonObject> lines = o.getCollection("code");
-                    if (lines != null) {
-                        for (JsonObject line : lines) {
-                            Code code = new Code();
-                            code.line = line.getInteger("line");
-                            code.type = line.getString("type");
-                            code.id = line.getString("id");
-                            code.level = line.getInteger("level");
-                            code.code = line.getString("code");
-                            code.description = line.getString("description");
-                            code.bodyTypes = line.getCollection("bodyTypes");
-                            code.bodyKinds = line.getCollection("bodyKinds");
-                            if (brief) {
-                                // make code brief
-                                code.code = StringHelper.before(code.code, 
"[", code.code);
-                            } else if (description) {
-                                // show description
-                                code.code = code.type + (code.description != 
null ? "[" + code.description + "]" : "");
-                            }
-                            row.code.add(code);
-                        }
-                    }
-                    boolean add = true;
-                    if (filter != null && !matchesFilter(filter, row.location, 
row.routeId)) {
-                        add = false;
-                    }
-                    if (add) {
-                        rows.add(row);
-                    }
-                }
+            JsonArray routes = new JsonArray();
+            for (Row row : rows) {
+                routes.add(row.json);
             }
-            // sort rows
-            rows.sort(ORDER.comparator(sort));
+            JsonObject answer = new JsonObject(jo);
+            answer.put("routes", routes);
+            addUnreadSourceParts(answer);
+            printer().println(Jsoner.prettyPrint(answer.toJson(), 2));
+        } else if (!rows.isEmpty()) {
+            printSource(rows);
+        }
+
+        return 0;
+    }
 
-            if (rows.isEmpty() && filter != null) {
-                throw CliException.notFound("No route matching: " + filter);
+    /**
+     * The routes of the dump, once each, that match the filter, with the code 
of their steps as --brief or
+     * --description asks for. Each row keeps its route as JSON with that 
code, for --json.
+     */
+    private List<Row> toRows(JsonObject jo) {
+        List<Row> rows = new ArrayList<>();
+        JsonArray arr = jo.getCollection("routes");
+        if (arr == null) {
+            return rows;
+        }
+        for (Object element : arr) {
+            JsonObject o = (JsonObject) element;
+            Row row = new Row();
+            row.location = extractSourceName(o.getString("source"));
+            row.routeId = o.getString("routeId");
+            row.checksum = o.getString("checksum");
+            if (rows.contains(row) || (filter != null && 
!matchesFilter(filter, row.location, row.routeId))) {
+                continue;
             }
-            if (!rows.isEmpty()) {
-                printSource(rows);
+            row.json = new JsonObject(o);
+            List<JsonObject> lines = o.getCollection("code");
+            if (lines != null) {
+                JsonArray jsonLines = new JsonArray();
+                for (JsonObject line : lines) {
+                    Code code = new Code();
+                    code.line = line.getInteger("line");
+                    code.type = line.getString("type");
+                    code.id = line.getString("id");
+                    code.level = line.getInteger("level");
+                    code.code = line.getString("code");
+                    code.description = line.getString("description");
+                    code.bodyTypes = line.getCollection("bodyTypes");
+                    code.bodyKinds = line.getCollection("bodyKinds");
+                    if (brief) {
+                        // make code brief
+                        code.code = StringHelper.before(code.code, "[", 
code.code);
+                    } else if (description) {
+                        // show description
+                        code.code = code.type + (code.description != null ? 
"[" + code.description + "]" : "");
+                    }
+                    row.code.add(code);
+                    JsonObject jsonLine = new JsonObject(line);
+                    jsonLine.put("code", code.code);
+                    jsonLines.add(jsonLine);
+                }
+                row.json.put("code", jsonLines);
             }
+            rows.add(row);
         }
-
-        return 0;
+        return rows;
     }
 
     /**
@@ -375,6 +396,8 @@ public class CamelRouteStructureAction extends 
ActionBaseCommand {
         String routeId;
         String checksum;
         List<Code> code = new ArrayList<>();
+        // the route as JSON, with the code of its steps as the text shows it
+        JsonObject json;
 
         @Override
         public boolean equals(Object o) {
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureActionTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureActionTest.java
index 803ba39ec9d7..2288bdd00935 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureActionTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/action/CamelRouteStructureActionTest.java
@@ -17,10 +17,15 @@
 package org.apache.camel.dsl.jbang.core.commands.action;
 
 import java.util.List;
+import java.util.function.Consumer;
 
 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.dsl.jbang.run.common.StringPrinter;
 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 org.junit.jupiter.api.extension.ExtendWith;
 import org.mockito.junit.jupiter.MockitoExtension;
@@ -28,6 +33,7 @@ import org.mockito.junit.jupiter.MockitoExtension;
 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.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 @ExtendWith(MockitoExtension.class)
@@ -165,6 +171,110 @@ class CamelRouteStructureActionTest extends 
ActionCommandTestSupport {
                 
CamelRouteStructureAction.bodyTypes(List.of("java.lang.String", 
"java.util.LinkedHashMap"), null));
     }
 
+    @Test
+    void testJsonHasOnlyTheRoutesOfTheFilter() throws Exception {
+        // --filter, --sort, --brief and --description were for the text only 
(CAMEL-26929)
+        JsonArray routes = jsonRoutes(c -> c.filter = "audit");
+
+        assertEquals(1, routes.size(), routes.toJson());
+        assertEquals("audit", ((JsonObject) 
routes.get(0)).getString("routeId"));
+    }
+
+    @Test
+    void testJsonIsSortedAndHasEachRouteOnce() throws Exception {
+        JsonArray byName = jsonRoutes(c -> c.sort = "name");
+        assertEquals(List.of("zeta", "audit"), routeIds(byName), "by the 
source file: a.camel.yaml, then b.camel.yaml");
+
+        JsonArray byId = jsonRoutes(c -> c.sort = "id");
+        // the route of the dump twice is in the JSON once, as in the text
+        assertEquals(List.of("audit", "zeta"), routeIds(byId));
+    }
+
+    @Test
+    void testJsonHasTheCodeOfBriefAndDescription() throws Exception {
+        JsonArray brief = jsonRoutes(c -> {
+            c.filter = "audit";
+            c.brief = true;
+        });
+        assertEquals(List.of("from", "to"), codes(brief));
+
+        JsonArray description = jsonRoutes(c -> {
+            c.filter = "audit";
+            c.description = true;
+        });
+        assertEquals(List.of("from", "to[Send to the audit log]"), 
codes(description));
+
+        JsonArray code = jsonRoutes(c -> c.filter = "audit");
+        assertEquals(List.of("from[timer://audit]", "to[log:audit]"), 
codes(code));
+    }
+
+    @Test
+    void testJsonOfAFilterThatMatchesNothingIsNotFound() throws Exception {
+        writeStatusFile(TEST_PID, "myApp");
+        CamelRouteStructureAction command = new CamelRouteStructureAction(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.sort = "name";
+        command.jsonOutput = true;
+        command.filter = "nope";
+
+        CliException e = assertThrows(CliException.class, () -> 
callWithResponse(command, twoRoutesResponse()));
+
+        assertEquals(CliExitCode.NOT_FOUND, e.getExitCode());
+        assertEquals("No route matching: nope", e.getMessage());
+        assertEquals("", printer.getOutput().trim(), "no JSON of all the 
routes");
+    }
+
+    private JsonArray jsonRoutes(Consumer<CamelRouteStructureAction> options) 
throws Exception {
+        printer = new StringPrinter();
+        writeStatusFile(TEST_PID, "myApp");
+        CamelRouteStructureAction command = new CamelRouteStructureAction(new 
CamelJBangMain().withPrinter(printer));
+        command.name = "myApp";
+        command.sort = "name";
+        command.jsonOutput = true;
+        options.accept(command);
+
+        assertEquals(0, callWithResponse(command, twoRoutesResponse()));
+        JsonObject jo = (JsonObject) Jsoner.deserialize(printer.getOutput());
+        return jo.getCollection("routes");
+    }
+
+    private static List<String> routeIds(JsonArray routes) {
+        return routes.stream().map(r -> ((JsonObject) 
r).getString("routeId")).toList();
+    }
+
+    private static List<String> codes(JsonArray routes) {
+        List<JsonObject> lines = ((JsonObject) 
routes.get(0)).getCollection("code");
+        return lines.stream().map(l -> l.getString("code")).toList();
+    }
+
+    /** zeta (in a.camel.yaml) and audit (in b.camel.yaml), and zeta once more 
as a dump may have it. */
+    private static JsonObject twoRoutesResponse() {
+        JsonArray zetaCode = new JsonArray();
+        zetaCode.add(codeLine(1, "from", "from1", 0, "from[timer://zeta]", 
null, null));
+        JsonObject zeta = new JsonObject();
+        zeta.put("source", "file:a.camel.yaml:1");
+        zeta.put("routeId", "zeta");
+        zeta.put("code", zetaCode);
+
+        JsonArray auditCode = new JsonArray();
+        auditCode.add(codeLine(1, "from", "from2", 0, "from[timer://audit]", 
null, null));
+        JsonObject to = codeLine(2, "to", "to1", 1, "to[log:audit]", null, 
null);
+        to.put("description", "Send to the audit log");
+        auditCode.add(to);
+        JsonObject audit = new JsonObject();
+        audit.put("source", "file:b.camel.yaml:1");
+        audit.put("routeId", "audit");
+        audit.put("code", auditCode);
+
+        JsonArray routes = new JsonArray();
+        routes.add(audit);
+        routes.add(zeta);
+        routes.add(new JsonObject(zeta));
+        JsonObject response = new JsonObject();
+        response.put("routes", routes);
+        return response;
+    }
+
     // Note: there is no no-match test here. When no pid matches, the command 
falls back to doCallSource, which
     // spawns a background Camel via Run to compile source files. That spawn 
path is out of scope for this batch
     // (it cannot be exercised deterministically in a unit test), so only the 
named-pid round-trip branch is covered.

Reply via email to