gnodet-bot commented on code in PR #26604:
URL: https://github.com/apache/camel/pull/26604#discussion_r4050388495


##########
dsl/camel-jbang/camel-jbang-core/src/main/java/org/apache/camel/dsl/jbang/core/common/ExampleHelper.java:
##########
@@ -135,15 +152,155 @@ private static boolean matches(JsonObject entry, String 
filter) {
         return false;
     }
 
+    /**
+     * The groups of the example ladder in reading order: level, title, and 
the one-line introduction the README of
+     * camel-jbang-examples uses. Quick start comes first, showcase last; a 
level not listed here sorts after them.
+     */
+    private static final String[][] GROUPS = {
+            {
+                    "quick-start", "Quick start",
+                    "The first ten minutes: generic examples with no story and 
no service, each running in seconds." },
+            { "run", "Run", "Running Camel: timers and cron schedules, a bean 
in a route, properties and profiles." },
+            { "transform", "Transform and map", "JSON, XML and CSV in and out, 
field-by-field mapping, Groovy and XSLT." },
+            {
+                    "route", "Route",
+                    "The routing patterns: content-based router, splitter, 
aggregator, filter and multicast." },
+            {
+                    "fail-well", "Fail well",
+                    "Retries, a dead letter channel, and a circuit breaker in 
front of a flaky service." },
+            {
+                    "connect", "Connect without a service",
+                    "Files, an HTTP client and a REST server; everything runs 
inside the example." },
+            {
+                    "connect-service", "Connect to one service",
+                    "SQL, JMS, MQTT, Kafka and FTP against a service the Camel 
CLI starts with camel infra run." },
+            {
+                    "contracts", "Contracts and security",
+                    "An OpenAPI contract served and called, and an API 
protected by Keycloak." },
+            { "ai", "AI", "A local model writing text, routes exposed as MCP 
tools, RAG over documents, PII redaction." },
+            {
+                    "cloud", "Cloud",
+                    "A cloud service, run locally through LocalStack and 
switched to the real thing by properties." },
+            {
+                    "showcase", "Showcase",
+                    "Tooling demos outside the ladder: the TUI, a memory leak, 
message sizes, log analysis." },
+    };
+
+    /**
+     * The levels of the ladder in reading order.
+     */
+    public static List<String> getGroupOrder() {
+        List<String> order = new ArrayList<>();
+        for (String[] g : GROUPS) {
+            order.add(g[0]);
+        }
+        return order;
+    }
+
+    /**
+     * The title of a group (level), for example "Quick start" for 
quick-start; an unknown level is capitalized.
+     */
+    public static String getGroupTitle(String level) {
+        for (String[] g : GROUPS) {
+            if (g[0].equals(level)) {
+                return g[1];
+            }
+        }
+        return formatCategory(level);
+    }
+
+    /**
+     * The one-line introduction of a group (level), or an empty string for an 
unknown level.
+     */
+    public static String getGroupIntro(String level) {
+        for (String[] g : GROUPS) {
+            if (g[0].equals(level)) {
+                return g[2];
+            }
+        }
+        return "";
+    }
+
+    /**
+     * Groups the examples by level in ladder order, each group sorted by 
name; empty groups are left out and levels not
+     * on the ladder come last in the order they appear.
+     */
+    public static Map<String, List<JsonObject>> groupByLevel(List<JsonObject> 
catalog) {
+        Map<String, List<JsonObject>> groups = new LinkedHashMap<>();
+        for (String level : getGroupOrder()) {
+            groups.put(level, new ArrayList<>());
+        }
+        for (JsonObject entry : catalog) {
+            String level = entry.getStringOrDefault("level", "other");
+            groups.computeIfAbsent(level, k -> new ArrayList<>()).add(entry);
+        }
+        groups.values().removeIf(List::isEmpty);
+        for (List<JsonObject> entries : groups.values()) {
+            entries.sort(Comparator.comparingInt(ExampleHelper::getOrder)
+                    .thenComparing(e -> e.getStringOrDefault("name", "")));
+        }
+        return groups;
+    }
+
+    /**
+     * The reading order of the example within its group from the metadata, or 
a large number when it has none, so
+     * examples with an order come first and the rest sort by name.
+     */
+    public static int getOrder(JsonObject entry) {
+        Object order = entry.get("order");
+        if (order instanceof Number n) {
+            return n.intValue();
+        }
+        return Integer.MAX_VALUE;
+    }
+
+    /**
+     * What the example teaches, as one line: the components and the EIPs from 
its metadata, or an empty string.
+     */
+    @SuppressWarnings("unchecked")
+    public static String getTeachesSummary(JsonObject entry) {
+        JsonObject teaches = entry.getMap("teaches");
+        if (teaches == null || teaches.isEmpty()) {
+            return "";
+        }
+        StringBuilder sb = new StringBuilder();
+        for (String key : new String[] { "components", "eips", "languages", 
"dataformats" }) {
+            Collection<String> values = (Collection<String>) teaches.get(key);

Review Comment:
   💡 **Unchecked cast without safety note.** `teaches.get(key)` returns 
`Object` from `JsonArray` (which extends `ArrayList<Object>`). Casting to 
`Collection<String>` is an erasure cast — the JVM only sees `Collection` at 
runtime. `String.join(CharSequence, Iterable<? extends CharSequence>)` will 
throw `ClassCastException` at runtime if any element is not a `CharSequence`.
   
   This is the same pattern used elsewhere in the file for `tags`, 
`infraServices`, and `files` (all guarded by `@SuppressWarnings("unchecked")`), 
and in practice the catalog JSON always has string arrays here. But there is no 
compile-time protection — a malformed catalog entry (e.g. `"components": [1, 
2]`) would blow up on the `--example` list command with an unhelpful stack 
trace.
   
   Consider a brief comment explaining why the cast is safe, or add a defensive 
check:
   ```java
   Object raw = teaches.get(key);
   if (!(raw instanceof Collection<?> col)) continue;
   for (Object item : col) {
       if (item instanceof String s) {
           if (sb.length() > 0) sb.append(", ");
           sb.append(s);
       }
   }
   ```



##########
dsl/camel-jbang/camel-jbang-core/src/test/java/org/apache/camel/dsl/jbang/core/common/ExampleHelperTest.java:
##########
@@ -125,31 +125,67 @@ void shouldGetFiles() {
     @Test
     void shouldExtractBundledExample() throws Exception {
         List<JsonObject> catalog = ExampleHelper.loadCatalog();
-        JsonObject entry = ExampleHelper.findExample(catalog, 
"eip/circuit-breaker");
+        JsonObject entry = ExampleHelper.findExample(catalog, 
"fail-well/circuit-breaker");
         Path tempDir = ExampleHelper.extractBundledExample(entry);
 
-        assertTrue(Files.exists(tempDir.resolve("route.camel.yaml")));
-        String content = Files.readString(tempDir.resolve("route.camel.yaml"));
+        
assertTrue(Files.exists(tempDir.resolve("circuit-breaker.camel.yaml")));
+        String content = 
Files.readString(tempDir.resolve("circuit-breaker.camel.yaml"));
         assertFalse(content.isEmpty());
     }
 
     @Test
-    void shouldExtractBundledExampleWithSubdirectory() throws Exception {
+    void shouldExtractBundledExampleWithJavaAndBeans() throws Exception {
         List<JsonObject> catalog = ExampleHelper.loadCatalog();
-        JsonObject entry = ExampleHelper.findExample(catalog, 
"transformation/xslt");
+        JsonObject entry = ExampleHelper.findExample(catalog, 
"quick-start/routes");
         Path tempDir = ExampleHelper.extractBundledExample(entry);
 
-        assertTrue(Files.exists(tempDir.resolve("consumer.camel.yaml")));
-        assertTrue(Files.exists(tempDir.resolve("stylesheet.xsl")));
-        assertTrue(Files.exists(tempDir.resolve("input/account.xml")));
+        assertTrue(Files.exists(tempDir.resolve("routes.camel.yaml")));
+        assertTrue(Files.exists(tempDir.resolve("Greeter.java")));
+        assertTrue(Files.exists(tempDir.resolve("beans.yaml")));
+    }
+
+    @Test
+    void shouldGroupByLevelInLadderOrder() {
+        List<JsonObject> catalog = ExampleHelper.loadCatalog();
+        java.util.Map<String, List<JsonObject>> groups = 
ExampleHelper.groupByLevel(catalog);
+        List<String> levels = new java.util.ArrayList<>(groups.keySet());
+        assertEquals("quick-start", levels.get(0));
+        assertTrue(levels.indexOf("route") < levels.indexOf("fail-well"));
+        assertTrue(levels.indexOf("connect") < 
levels.indexOf("connect-service"));
+        assertEquals("showcase", levels.get(levels.size() - 1));
+        assertEquals("Quick start", 
ExampleHelper.getGroupTitle("quick-start"));
+        assertEquals("Connect to one service", 
ExampleHelper.getGroupTitle("connect-service"));
+        assertFalse(ExampleHelper.getGroupIntro("fail-well").isEmpty());
+        for (List<JsonObject> entries : groups.values()) {
+            assertFalse(entries.isEmpty());
+        }
+    }
+
+    @Test
+    void shouldSummarizeTeachesAndCiSkip() {
+        List<JsonObject> catalog = ExampleHelper.loadCatalog();
+        JsonObject aggregator = ExampleHelper.findExample(catalog, 
"route/aggregator");
+        String teaches = ExampleHelper.getTeachesSummary(aggregator);
+        assertTrue(teaches.contains("eips: "), teaches);
+        assertTrue(teaches.contains("aggregate"), teaches);
+        assertFalse(ExampleHelper.isCiSkip(aggregator));
+        JsonObject chat = ExampleHelper.findExample(catalog, 
"ai/langchain4j-chat");
+        assertTrue(ExampleHelper.isCiSkip(chat));
+    }
+
+    @Test
+    void shouldFindAmbiguousShortNames() {
+        List<JsonObject> catalog = ExampleHelper.loadCatalog();
+        assertEquals(1, ExampleHelper.findExamplesByShortName(catalog, 
"aggregator").size());
+        assertTrue(ExampleHelper.findExamplesByShortName(catalog, 
"no-such-example").isEmpty());

Review Comment:
   💡 **Test covers 1-match and 0-match, but not the 2-match case.** 
`findExamplesByShortName` is only called in production to detect ambiguity 
(size > 1) and display the error in `runExample()`. The test only asserts `size 
== 1` (for `"aggregator"`) and `size == 0` (for unknown name). Neither path 
exercises the error message that fires when two examples share a short name.
   
   Since the current catalog has no duplicate short names this is not a defect 
today, but the test gives false confidence. A minimal additional assertion:
   ```java
   // synthetic ambiguity — the 2-match case feeds the error path in Run.java
   JsonObject a = new JsonObject(); a.put("name", "group-a/foo");
   JsonObject b = new JsonObject(); b.put("name", "group-b/foo");
   assertEquals(2, ExampleHelper.findExamplesByShortName(List.of(a, b), 
"foo").size());
   ```



##########
dsl/camel-jbang/camel-jbang-plugin-tui/src/main/java/org/apache/camel/dsl/jbang/core/commands/tui/ExampleBrowserPopup.java:
##########
@@ -139,9 +138,22 @@ boolean handleKeyEvent(KeyEvent ke) {
             return true;
         }
         if (ke.isChar('d')) {
-            loadDocFromExample();
+            loadDocForSelected();
             return true;
         }
+        if (currentFolder == null) {
+            // 1 to 9 open the first nine groups, 0 the tenth
+            for (char c = '0'; c <= '9'; c++) {
+                if (ke.isChar(c)) {
+                    int n = c == '0' ? 10 : c - '0';
+                    List<String> levels = new 
ArrayList<>(ExampleHelper.groupByLevel(catalog).keySet());

Review Comment:
   💡 **`groupByLevel(catalog)` called up to 4× per render frame.** 
`handleKeyEvent` (here), `buildTopLevelItems`, `buildFolderItems`, and 
`folderExampleCount` each call `ExampleHelper.groupByLevel(catalog)`, which 
builds a fresh `LinkedHashMap` by iterating and sorting the full catalog.
   
   The catalog is ~40 entries today so the wall-clock cost is negligible, but 
the pattern is wrong: the grouped result is a pure function of `catalog`, which 
only changes on `reload()`. Cache it:
   ```java
   private Map<String, List<JsonObject>> groupedCatalog;
   
   void reload() {
       this.catalog = loadAndSortExamples();
       this.groupedCatalog = ExampleHelper.groupByLevel(catalog);
   }
   ```
   Then replace all four `ExampleHelper.groupByLevel(catalog)` call-sites with 
`groupedCatalog`.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to