Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(cli): pretty-print cargo loco routes #1073

Merged
merged 1 commit into from
Dec 13, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
11 changes: 9 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
Expand Down
86 changes: 49 additions & 37 deletions examples/demo/tests/cmd/cli.trycmd
Original file line number Diff line number Diff line change
Expand Up @@ -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

```

Expand Down
100 changes: 97 additions & 3 deletions src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -790,9 +791,102 @@ pub async fn main<H: Hooks>() -> crate::Result<()> {

fn show_list_endpoints<H: Hooks>(ctx: &AppContext) {
let mut routes = list_endpoints::<H>(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<String, BTreeMap<String, Vec<String>>> = 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::<Vec<_>>()
.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(),
}
}

Expand Down
Loading