diff --git a/CHANGELOG.md b/CHANGELOG.md index 13e008e6a..c128ab926 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Unreleased +* feat: `cargo loco routes` will now pretty-print routes * fix: guard jwt error behind feature flag. [https://github.com/loco-rs/loco/pull/1032](https://github.com/loco-rs/loco/pull/1032) * fix: logger file_appender not using the seperated format setting. [https://github.com/loco-rs/loco/pull/1036](https://github.com/loco-rs/loco/pull/1036) * seed cli command. [https://github.com/loco-rs/loco/pull/1046](https://github.com/loco-rs/loco/pull/1046) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9115ea351..3447e9adb 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -54,12 +54,19 @@ Just clone the project and run `cargo test`. You can see how we test in [.github/workflows](.github/workflows/) #### Snapshots -To update/create a snapshots we are using [insta](https://github.com/mitsuhiko/insta). all you need to do is install insta (cargo install cargo-insta) and run the following command: +We use [insta](https://github.com/mitsuhiko/insta) for snapshot testing, which helps us detect changes in output formats and behavior. To work with snapshots: + +1. Install the insta CLI tool: +```sh +cargo install cargo-insta ``` + +2. Run tests and review/update snapshots: +```sh cargo insta test --review ``` -In case of cli changes we snapshot the binary commands. in case of changes run the following command yo update the CLI snapshot +For CLI-related changes, we maintain separate snapshots of binary command outputs. To update these CLI snapshots: ```sh LOCO_CI_MODE=true TRYCMD=overwrite cargo test ``` diff --git a/examples/demo/tests/cmd/cli.trycmd b/examples/demo/tests/cmd/cli.trycmd index ca64094ba..6eeff130a 100644 --- a/examples/demo/tests/cmd/cli.trycmd +++ b/examples/demo/tests/cmd/cli.trycmd @@ -87,43 +87,55 @@ user_report [output a user report] ```console $ demo_app-cli routes --environment test -[GET] /_health -[GET] /_ping -[POST] /auth/forgot -[POST] /auth/login -[POST] /auth/register -[POST] /auth/reset -[POST] /auth/verify -[GET] /cache -[GET] /cache/get_or_insert -[POST] /cache/insert -[GET] /mylayer/admin -[GET] /mylayer/echo -[GET] /mylayer/user -[GET] /mysession -[GET] /notes -[POST] /notes -[GET] /notes/:id -[DELETE] /notes/:id -[POST] /notes/:id -[GET] /response/album -[GET] /response/empty -[GET] /response/empty_json -[GET] /response/etag -[GET] /response/html -[GET] /response/json -[GET] /response/redirect -[GET] /response/render_with_status_code -[GET] /response/set_cookie -[GET] /response/text -[POST] /upload/file -[POST] /user/convert/admin -[POST] /user/convert/user -[GET] /user/current -[GET] /user/current_api_key -[GET] /view-engine/hello -[GET] /view-engine/home -[GET] /view-engine/simple +/_health + └─ GET /_health +/_ping + └─ GET /_ping +/auth + ├─ POST /auth/forgot + ├─ POST /auth/login + ├─ POST /auth/register + ├─ POST /auth/reset + └─ POST /auth/verify +/cache + ├─ GET /cache + ├─ GET /cache/get_or_insert + └─ POST /cache/insert +/mylayer + ├─ GET /mylayer/admin + ├─ GET /mylayer/echo + └─ GET /mylayer/user +/mysession + └─ GET /mysession +/notes + ├─ GET /notes + │ POST /notes + │ + ├─ GET /notes/:id + │ POST /notes/:id + └─ DELETE /notes/:id +/response + ├─ GET /response/album + ├─ GET /response/empty + ├─ GET /response/empty_json + ├─ GET /response/etag + ├─ GET /response/html + ├─ GET /response/json + ├─ GET /response/redirect + ├─ GET /response/render_with_status_code + ├─ GET /response/set_cookie + └─ GET /response/text +/upload + └─ POST /upload/file +/user + ├─ POST /user/convert/admin + ├─ POST /user/convert/user + ├─ GET /user/current + └─ GET /user/current_api_key +/view-engine + ├─ GET /view-engine/hello + ├─ GET /view-engine/home + └─ GET /view-engine/simple ``` diff --git a/src/cli.rs b/src/cli.rs index d380c8003..7cb613b59 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -24,9 +24,10 @@ cfg_if::cfg_if! { } else {} } -use std::path::PathBuf; +use std::{collections::BTreeMap, path::PathBuf}; use clap::{ArgAction, Parser, Subcommand}; +use colored::Colorize; use duct::cmd; use loco_gen::{Component, ScaffoldKind}; @@ -790,9 +791,102 @@ pub async fn main() -> crate::Result<()> { fn show_list_endpoints(ctx: &AppContext) { let mut routes = list_endpoints::(ctx); - routes.sort_by(|a, b| a.uri.cmp(&b.uri)); + + // Sort first by path, then ensure HTTP methods are in a consistent order + routes.sort_by(|a, b| { + let method_priority = |actions: &[_]| match actions + .first() + .map(ToString::to_string) + .unwrap_or_default() + .as_str() + { + "GET" => 0, + "POST" => 1, + "PUT" => 2, + "PATCH" => 3, + "DELETE" => 4, + _ => 5, + }; + + let a_priority = method_priority(&a.actions); + let b_priority = method_priority(&b.actions); + + a.uri.cmp(&b.uri).then(a_priority.cmp(&b_priority)) + }); + + // Group routes by their first path segment and full path + let mut path_groups: BTreeMap>> = BTreeMap::new(); + for router in routes { - println!("{router}"); + let path = router.uri.trim_start_matches('/'); + let segments: Vec<&str> = path.split('/').collect(); + let root = (*segments.first().unwrap_or(&"")).to_string(); + + let actions_str = router + .actions + .iter() + .map(ToString::to_string) + .collect::>() + .join(","); + + path_groups + .entry(root) + .or_default() + .entry(router.uri.to_string()) + .or_default() + .push(actions_str); + } + + // Print tree structure + for (root, paths) in path_groups { + println!("/{}", root.bold()); + let paths_count = paths.len(); + let mut path_idx = 0; + + for (path, methods) in paths { + path_idx += 1; + let is_last_path = path_idx == paths_count; + let is_group = methods.len() > 1; + + // Print first method + let prefix = if is_last_path && !is_group { + " └─ " + } else { + " ├─ " + }; + let colored_method = color_method(&methods[0]); + println!("{prefix}{colored_method}\t{path}"); + + // Print additional methods in group + if is_group { + for (i, method) in methods[1..].iter().enumerate() { + let is_last_in_group = i == methods.len() - 2; + let group_prefix = if is_last_path && is_last_in_group { + " └─ " + } else { + " │ " + }; + let colored_method = color_method(method); + println!("{group_prefix}{colored_method}\t{path}"); + } + + // Add spacing between groups if not the last path + if !is_last_path { + println!(" │"); + } + } + } + } +} + +fn color_method(method: &str) -> String { + match method { + "GET" => method.green().to_string(), + "POST" => method.blue().to_string(), + "PUT" => method.yellow().to_string(), + "PATCH" => method.magenta().to_string(), + "DELETE" => method.red().to_string(), + _ => method.to_string(), } }