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.