This is an automated email from the ASF dual-hosted git repository.

davsclaus pushed a commit to branch tui-walk-examples
in repository https://gitbox.apache.org/repos/asf/camel.git

commit 398c13b1c05723dd251c9a9b2b267a72f82cc218
Author: Claus Ibsen <[email protected]>
AuthorDate: Tue Nov 10 23:03:19 2026 +0100

    chore: camel-jbang - About names a long route and the route of the API 
description in full, and an encoded out is the one the routes name
    
    From walking the route, connect and contracts examples:
    
    - What happened shows content-based-router in full when the strips leave
      room, not content-based-rout....
    - The route Camel adds to serve the API description of the REST DSL
      (route1 from rest-api://openapi) is named by its endpoint on its strip.
    - AboutReport.baseUri decodes the escapes Camel normalizes a URI with, so
      rest-openapi://stock-api.json%23reserveStock is the
      rest-openapi:stock-api.json#reserveStock the route names: openapi-client
      had its out twice, once without its calls.
    
    Co-Authored-By: Claude Opus 5.5 <[email protected]>
    Signed-off-by: Claus Ibsen <[email protected]>
---
 .../dsl/jbang/core/commands/ai/AboutReport.java    | 18 ++++++
 .../dsl/jbang/core/commands/ai/AboutTest.java      |  5 ++
 .../dsl/jbang/core/commands/tui/AboutView.java     | 26 ++++++++-
 .../dsl/jbang/core/commands/tui/AboutViewTest.java | 67 ++++++++++++++++++++++
 .../jbang/core/commands/tui/ObservedOutsTest.java  | 16 ++++++
 5 files changed, 130 insertions(+), 2 deletions(-)

diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
index a99b40430c9e..45d5396e885f 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutReport.java
@@ -17,6 +17,8 @@
 package org.apache.camel.dsl.jbang.core.commands.ai;
 
 import java.io.IOException;
+import java.net.URLDecoder;
+import java.nio.charset.StandardCharsets;
 import java.nio.file.Path;
 import java.util.ArrayList;
 import java.util.Arrays;
@@ -347,9 +349,25 @@ public final class AboutReport {
         int q = uri.indexOf('?');
         String answer = q >= 0 ? uri.substring(0, q) : uri;
         answer = answer.replaceFirst("^([a-zA-Z0-9+.-]+):/{0,3}", "$1:");
+        // the runtime has the URI encoded as Camel normalizes it 
(rest-openapi://stock-api.json%23reserveStock,
+        // platform-http:///stock/%7Bsku%7D), the sources as written
+        answer = decoded(answer);
         return answer.endsWith("/") ? answer.substring(0, answer.length() - 1) 
: answer;
     }
 
+    private static String decoded(String uri) {
+        if (uri.indexOf('%') < 0) {
+            return uri;
+        }
+        try {
+            // a + is a plus in a path, not a space
+            return URLDecoder.decode(uri.replace("+", "%2B"), 
StandardCharsets.UTF_8);
+        } catch (IllegalArgumentException e) {
+            // a % that is no escape
+            return uri;
+        }
+    }
+
     private static String total(JsonObject jo) {
         if (jo.get("exchangesTotal") == null) {
             return "";
diff --git 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
index 73b26d7c27c3..ec0fdf4ff262 100644
--- 
a/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/commands/ai/AboutTest.java
@@ -176,6 +176,11 @@ class AboutTest {
         
assertThat(AboutReport.baseUri("file:/tmp/in?noop=true")).isEqualTo(AboutReport.baseUri("file:///tmp/in"));
         
assertThat(AboutReport.baseUri("kafka://orders")).isEqualTo("kafka:orders");
         
assertThat(AboutReport.baseUri("direct:orders")).isEqualTo("direct:orders");
+        // the runtime has it encoded, the sources as written 
(contracts/openapi-client, connect/stock-api)
+        
assertThat(AboutReport.baseUri("rest-openapi://stock-api.json%23reserveStock"))
+                
.isEqualTo(AboutReport.baseUri("rest-openapi:stock-api.json#reserveStock"));
+        
assertThat(AboutReport.baseUri("platform-http:///stock/%7Bsku%7D";)).isEqualTo("platform-http:stock/{sku}");
+        
assertThat(AboutReport.baseUri("http:shop/a+b/100%")).isEqualTo("http:shop/a+b/100%");
     }
 
     @Test
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
index 80058d5e2230..fc122d35962e 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutView.java
@@ -485,6 +485,24 @@ final class AboutView {
      * The routes in the order of the Routes panel, where a route comes after 
the one that calls it, then those it does
      * not have; the integration lists its routes by id, which puts a called 
route before its caller.
      */
+    /**
+     * The name of a route on its strip: its id, or for a route the sources do 
not have and Camel named route1, its
+     * endpoint (rest-api:openapi, the route that serves the API description 
of the REST DSL).
+     */
+    private String stripName(IntegrationInfo info, String routeId) {
+        About.Model m = model;
+        if (m == null || !routeId.matches("route\\d+") || 
m.routes().stream().anyMatch(r -> routeId.equals(r.routeId()))) {
+            return routeId;
+        }
+        String from = info.routes.stream().filter(r -> 
routeId.equals(r.routeId) && r.from != null).map(r -> r.from)
+                .findFirst().orElse(null);
+        if (from == null) {
+            return routeId;
+        }
+        int q = from.indexOf('?');
+        return (q >= 0 ? from.substring(0, q) : from).replaceFirst("://", ":");
+    }
+
     private List<String> routeOrder(IntegrationInfo info) {
         List<String> running = info.routes.stream().map(r -> 
r.routeId).filter(id -> id != null).toList();
         List<String> order = new ArrayList<>();
@@ -720,7 +738,8 @@ final class AboutView {
         int seconds = seconds();
         List<String> order = routeOrder(info);
         // the names, then one space: every line starts its columns at the 
same place
-        int label = Math.min(18, Math.max(8, 
order.stream().mapToInt(String::length).max().orElse(8))) + 4;
+        int longest = Math.max(8, order.stream().mapToInt(id -> 
stripName(info, id).length()).max().orElse(8));
+        int label = Math.min(18, longest) + 4;
         List<Timeline.RouteSeries> series = ctx.timeline.routes(info.pid, 
order, end, seconds);
         // what the line of a route has after its strip (its exchanges, an 
open circuit breaker, the worst that
         // happened, with the cause of a recovery up to CAUSE_WIDTH) takes the 
room it needs, and the strips the rest
@@ -729,6 +748,8 @@ final class AboutView {
             notes = Math.max(notes, width(stripNotes(info, r, last, end, 
seconds, CAUSE_WIDTH)));
         }
         int columns = stripColumns(seconds, width - label - notes - 1);
+        // a longer name in full, in what the strips leave 
(content-based-router)
+        label += Math.max(0, Math.min(longest + 4 - label, width - label - 
notes - 1 - columns));
         // the cause gets what the strips leave too
         int causeRoom = CAUSE_WIDTH + Math.max(0, width - label - notes - 1 - 
columns);
         stripColumns = columns;
@@ -741,7 +762,8 @@ final class AboutView {
             if (spaced && !lines.isEmpty()) {
                 lines.add(Line.from(Span.raw("")));
             }
-            String name = r.route().length() > label - 3 ? 
r.route().substring(0, label - 4) + "…" : r.route();
+            String name = stripName(info, r.route());
+            name = name.length() > label - 3 ? name.substring(0, label - 4) + 
"…" : name;
             long max = 1;
             // completed, recovered, handled, failed, down, redelivery 
attempts: the height is the exchanges done, the
             // attempts only colour the column (an exchange retried many times 
is still one exchange)
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
index f748a70566ba..1f8c6b769ee4 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/AboutViewTest.java
@@ -444,6 +444,73 @@ class AboutViewTest {
         assertThat(about.tableJson(info).getString("keys")).contains("r the 
README, s the summary");
     }
 
+    @Test
+    void theRouteCamelAddsForTheApiDescriptionIsNamedByItsEndpoint() throws 
Exception {
+        // contracts/rest-code-first: the REST DSL serves /openapi from a 
route Camel adds and names route1
+        Files.writeString(dir.resolve("rest-code-first.camel.yaml"), """
+                - route:
+                    id: list-orders
+                    from:
+                      uri: direct:list-orders
+                      steps:
+                        - log: listed
+                """);
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "rest-code-first";
+        info.directory = dir.toString();
+        for (String[] r : List.of(new String[] { "list-orders", 
"direct://list-orders" },
+                new String[] { "route1", "rest-api://openapi" })) {
+            RouteInfo route = new RouteInfo();
+            route.routeId = r[0];
+            route.from = r[1];
+            route.state = "Started";
+            info.routes.add(route);
+        }
+        TimelineJson t = new TimelineJson(T0 / 1000 + 59, 60);
+        t.route("list-orders", "completed", 55, 1).route("route1", 
"completed", 56, 1);
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        ctx.timeline.merge("1234", t.json(), true);
+        AboutView about = new AboutView(ctx);
+
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> {
+            List<String> strips = render(about, info, 200, 
34).lines().dropWhile(l -> !l.contains("What happened"))
+                    .filter(l -> l.contains("    list-orders ") || 
l.contains("    rest-api:openapi ")).toList();
+            assertThat(strips).hasSize(2);
+        });
+        assertThat(render(about, info, 200, 34)).doesNotContain(" route1 ");
+    }
+
+    @Test
+    void aLongRouteNameIsShownInFullWhenTheStripsLeaveRoom() throws Exception {
+        // route/content-based-router: its one route has the name of the 
example
+        Files.writeString(dir.resolve("content-based-router.camel.yaml"), """
+                - route:
+                    id: content-based-router
+                    from:
+                      uri: file:orders
+                      steps:
+                        - log: routed
+                """);
+        IntegrationInfo info = new IntegrationInfo();
+        info.pid = "1234";
+        info.name = "content-based-router";
+        info.directory = dir.toString();
+        RouteInfo route = new RouteInfo();
+        route.routeId = "content-based-router";
+        route.state = "Started";
+        info.routes.add(route);
+        TimelineJson t = new TimelineJson(T0 / 1000 + 59, 60);
+        t.route("content-based-router", "completed", 55, 3);
+        MonitorContext ctx = new MonitorContext(new 
AtomicReference<>(List.of(info)), new AtomicReference<>(List.of()));
+        ctx.timeline.merge("1234", t.json(), true);
+        AboutView about = new AboutView(ctx);
+
+        await().atMost(20, TimeUnit.SECONDS).untilAsserted(() -> 
assertThat(render(about, info, 200, 34).lines()
+                .dropWhile(l -> !l.contains("What happened")).filter(l -> 
l.contains("content-based-rout")).toList())
+                .singleElement().satisfies(l -> 
assertThat(l).contains("content-based-router ").doesNotContain("…")));
+    }
+
     @Test
     void lastFiveMinutesCountsAnExchangeOnceNotOncePerRouteItPasses() throws 
Exception {
         // fail-well/circuit-breaker: stock-check calls supplier with direct, 
once a second
diff --git 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
index d7f9c3f9af73..3985234d4034 100644
--- 
a/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
+++ 
b/dsl/camel-jbang/camel-jbang-plugin-tui/src/test/java/org/apache/camel/dsl/jbang/core/commands/tui/ObservedOutsTest.java
@@ -64,6 +64,22 @@ class ObservedOutsTest {
         assertThat(ObservedOuts.of(info, 
List.of())).extracting(About.Out::uri).containsExactly("kafka:audit");
     }
 
+    @Test
+    void anOutTheRuntimeHasEncodedIsTheOneOfTheRoutes() {
+        // contracts/openapi-client: the route names 
rest-openapi:stock-api.json#reserveStock, the runtime counts it
+        // as rest-openapi://stock-api.json%23reserveStock
+        IntegrationInfo info = new IntegrationInfo();
+        info.endpoints.add(endpoint("reserve-order-lines", "out", 
"rest-openapi://stock-api.json%23reserveStock",
+                "remote", 6));
+        List<About.Out> known = List.of(new About.Out(
+                "http", "rest-openapi:stock-api.json#reserveStock", "out",
+                "reserve-order-lines"));
+
+        assertThat(ObservedOuts.of(info, known)).isEmpty();
+        assertThat(AboutView.outHits(info, 
"rest-openapi:stock-api.json#reserveStock", "reserve-order-lines"))
+                .isEqualTo(6);
+    }
+
     @Test
     void theServiceOfAUriIsAllButItsLastSegment() {
         
assertThat(ObservedOuts.group("http://localhost:8080/stock/CAMEL-MUG";)).isEqualTo("http://localhost:8080/stock/";);

Reply via email to