use crate::error::CliError;
use serde::Serialize;
use std::fs;
use std::path::Path;
#[derive(Debug, Serialize)]
struct RouteInfo {
method: String,
path: String,
handler: String,
middleware: Vec<String>,
guards: Vec<String>,
}
#[derive(Debug, Clone, Copy)]
pub enum RouteFormat {
Table,
Json,
Yaml,
Markdown,
}
pub fn execute(project_dir: Option<&str>) -> Result<(), CliError> {
run(project_dir, None, None, RouteFormat::Table, false)
}
pub fn execute_with_filter(
project_dir: Option<&str>,
method: Option<&str>,
) -> Result<(), CliError> {
run(project_dir, method, None, RouteFormat::Table, false)
}
pub fn run(
project_dir: Option<&str>,
method: Option<&str>,
path: Option<&str>,
format: RouteFormat,
show_middleware: bool,
) -> Result<(), CliError> {
let dir = project_dir.unwrap_or(".");
let mut routes = find_routes(dir)?;
if let Some(m) = method {
routes.retain(|r| r.method.eq_ignore_ascii_case(m));
}
if let Some(p) = path {
routes.retain(|r| r.path.contains(p));
}
match format {
RouteFormat::Table => render_table(&routes, method, path, show_middleware),
RouteFormat::Json => render_json(&routes)?,
RouteFormat::Yaml => render_yaml(&routes)?,
RouteFormat::Markdown => render_markdown(&routes, show_middleware),
}
Ok(())
}
fn find_routes(dir: &str) -> Result<Vec<RouteInfo>, CliError> {
let mut routes = Vec::new();
let paths_to_search = vec![
format!("{}/src/main.rs", dir),
format!("{}/src/routes.rs", dir),
format!("{}/src/routes/mod.rs", dir),
];
for path in paths_to_search {
if Path::new(&path).exists()
&& let Ok(content) = fs::read_to_string(&path)
{
routes.extend(parse_routes(&content));
}
}
let controllers_dir = format!("{}/src/controllers", dir);
if let Ok(entries) = fs::read_dir(&controllers_dir) {
let mut files: Vec<_> = entries.flatten().map(|e| e.path()).collect();
files.sort();
for file in files {
if let Ok(content) = fs::read_to_string(&file) {
routes.extend(parse_routes(&content));
}
}
}
Ok(routes)
}
fn parse_routes(content: &str) -> Vec<RouteInfo> {
let mut routes = Vec::new();
let mut pending_middleware: Vec<String> = Vec::new();
let mut pending_guards: Vec<String> = Vec::new();
for line in content.lines() {
let line = line.trim();
if let Some(names) = parse_attr_list(line, "middleware") {
pending_middleware.extend(names);
continue;
}
if let Some(names) = parse_attr_list(line, "guard") {
pending_guards.extend(names);
continue;
}
if line.starts_with("#[")
&& line.contains("(\"")
&& let Some(mut route) = parse_route_decorator(line)
{
route.middleware = std::mem::take(&mut pending_middleware);
route.guards = std::mem::take(&mut pending_guards);
routes.push(route);
}
}
routes
}
fn parse_attr_list(line: &str, attr: &str) -> Option<Vec<String>> {
let prefix = format!("#[{}(", attr);
let rest = line.strip_prefix(&prefix)?;
let inner = rest.strip_suffix(")]").unwrap_or(rest);
let names: Vec<String> = inner
.split(',')
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty())
.collect();
Some(names)
}
fn parse_route_decorator(line: &str) -> Option<RouteInfo> {
let methods = vec!["get", "post", "put", "delete", "patch", "head", "options"];
for method in methods {
let pattern = format!("#[{}(\"", method);
if line.contains(&pattern)
&& let Some(start) = line.find("(\"")
&& let Some(end) = line[start..].find("\")")
{
let path = &line[start + 2..start + end];
return Some(RouteInfo {
method: method.to_uppercase(),
path: path.to_string(),
handler: "handler".to_string(),
middleware: Vec::new(),
guards: Vec::new(),
});
}
}
None
}
fn no_routes_message() {
println!("No routes found.");
println!();
println!("Routes are typically defined in:");
println!(" - src/main.rs");
println!(" - src/routes.rs");
println!(" - src/controllers/*.rs");
}
fn render_table(
routes: &[RouteInfo],
method: Option<&str>,
path: Option<&str>,
show_middleware: bool,
) {
println!("🗺️ Armature Routes");
if let Some(m) = method {
println!(" Filtered by method: {}", m.to_uppercase());
}
if let Some(p) = path {
println!(" Filtered by path: {}", p);
}
println!("==================");
println!();
if routes.is_empty() {
no_routes_message();
return;
}
let method_width = routes
.iter()
.map(|r| r.method.len())
.max()
.unwrap_or(6)
.max(6);
let path_width = routes
.iter()
.map(|r| r.path.len())
.max()
.unwrap_or(4)
.max(4);
println!("{:width$} PATH", "METHOD", width = method_width);
println!("{}", "-".repeat(method_width + path_width + 2));
for route in routes {
println!(
"{:width$} {}",
route.method,
route.path,
width = method_width
);
if show_middleware || !route.middleware.is_empty() {
if route.middleware.is_empty() {
println!(" └─ Middleware: (none)");
} else {
println!(" └─ Middleware: {}", route.middleware.join(", "));
}
}
if !route.guards.is_empty() {
println!(" └─ Guards: {}", route.guards.join(", "));
}
}
println!();
println!("Statistics:");
println!(" Total routes: {}", routes.len());
let methods: std::collections::HashSet<_> = routes.iter().map(|r| &r.method).collect();
println!(" HTTP methods: {}", methods.len());
let with_middleware = routes.iter().filter(|r| !r.middleware.is_empty()).count();
println!(" Routes with middleware: {}", with_middleware);
let with_guards = routes.iter().filter(|r| !r.guards.is_empty()).count();
println!(" Routes with guards: {}", with_guards);
}
fn render_json(routes: &[RouteInfo]) -> Result<(), CliError> {
let json = serde_json::to_string_pretty(routes)
.map_err(|e| CliError::Command(format!("Failed to serialize routes to JSON: {}", e)))?;
println!("{}", json);
Ok(())
}
fn render_yaml(routes: &[RouteInfo]) -> Result<(), CliError> {
let yaml = serde_yaml::to_string(routes)
.map_err(|e| CliError::Command(format!("Failed to serialize routes to YAML: {}", e)))?;
print!("{}", yaml);
Ok(())
}
fn render_markdown(routes: &[RouteInfo], show_middleware: bool) {
println!("# Routes");
println!();
if routes.is_empty() {
println!("_No routes found._");
return;
}
if show_middleware {
println!("| Method | Path | Middleware | Guards |");
println!("| ------ | ---- | ---------- | ------ |");
for r in routes {
println!(
"| {} | {} | {} | {} |",
r.method,
r.path,
if r.middleware.is_empty() {
"-".to_string()
} else {
r.middleware.join(", ")
},
if r.guards.is_empty() {
"-".to_string()
} else {
r.guards.join(", ")
},
);
}
} else {
println!("| Method | Path |");
println!("| ------ | ---- |");
for r in routes {
println!("| {} | {} |", r.method, r.path);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_route_decorator() {
let line = r#"#[get("/api/users")]"#;
let route = parse_route_decorator(line).unwrap();
assert_eq!(route.method, "GET");
assert_eq!(route.path, "/api/users");
}
#[test]
fn test_parse_post_route() {
let line = r#"#[post("/api/users")]"#;
let route = parse_route_decorator(line).unwrap();
assert_eq!(route.method, "POST");
assert_eq!(route.path, "/api/users");
}
#[test]
fn test_parse_attr_list() {
assert_eq!(
parse_attr_list("#[middleware(LoggerMiddleware)]", "middleware"),
Some(vec!["LoggerMiddleware".to_string()])
);
assert_eq!(
parse_attr_list("#[middleware(A, B)]", "middleware"),
Some(vec!["A".to_string(), "B".to_string()])
);
assert_eq!(parse_attr_list("#[get(\"/x\")]", "middleware"), None);
}
#[test]
fn parses_middleware_and_guards_onto_next_route() {
let content = r#"
#[middleware(LoggerMiddleware)]
#[guard(AuthGuard)]
#[get("/api/users")]
pub async fn list() {}
#[post("/api/users")]
pub async fn create() {}
"#;
let routes = parse_routes(content);
assert_eq!(routes.len(), 2);
assert_eq!(routes[0].middleware, vec!["LoggerMiddleware".to_string()]);
assert_eq!(routes[0].guards, vec!["AuthGuard".to_string()]);
assert!(routes[1].middleware.is_empty());
assert!(routes[1].guards.is_empty());
}
}