Skip to content

Commit

Permalink
Merge pull request #1073 from AngelOnFira/add-prettytable
Browse files Browse the repository at this point in the history
feat(cli): pretty-print cargo loco routes
  • Loading branch information
jondot authored Dec 13, 2024
2 parents 3bc8c07 + b42cd12 commit a4ca2e9
Show file tree
Hide file tree
Showing 4 changed files with 156 additions and 42 deletions.
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

0 comments on commit a4ca2e9

Please sign in to comment.