rok-cli 0.3.2

Developer CLI for rok-based Axum applications
//! `rok routes:list` — scan `src/routes/` and print a formatted route table.
//!
//! Reads the route source files and extracts `.route(...)` declarations rather
//! than requiring the app binary to be compiled and running.

use std::{fs, path::Path};

pub fn run() -> anyhow::Result<()> {
    let routes_dir = Path::new("src/routes");
    if !routes_dir.exists() {
        anyhow::bail!("src/routes/ not found. Run `rok routes:list` from the project root.");
    }

    println!("{:<8} {:<40} HANDLER", "METHOD", "PATH");
    println!("{}", "-".repeat(80));

    let mut found = false;
    for entry in fs::read_dir(routes_dir)? {
        let entry = entry?;
        let path = entry.path();
        if path.extension().and_then(|e| e.to_str()) != Some("rs") {
            continue;
        }
        let src = fs::read_to_string(&path)?;
        extract_routes(&src, &path.display().to_string());
        found = true;
    }

    if !found {
        println!("  (no route files found in src/routes/)");
    }

    Ok(())
}

/// Very lightweight regex-free route extractor.
/// Matches `.route("/path", METHOD(Handler::method))` patterns.
fn extract_routes(src: &str, file: &str) {
    for line in src.lines() {
        let trimmed = line.trim();
        if !trimmed.starts_with(".route(") {
            continue;
        }

        // Parse: .route("/path", get(Handler::fn))
        // or:    .route("/path", get(fn).post(fn2))
        let inner = trimmed
            .trim_start_matches(".route(")
            .trim_end_matches(')')
            .trim_end_matches(',');

        let mut parts = inner.splitn(2, ',');
        let path = parts
            .next()
            .unwrap_or("")
            .trim()
            .trim_matches('"')
            .to_string();
        let handlers_str = parts.next().unwrap_or("").trim().to_string();

        for (method, handler) in parse_method_handlers(&handlers_str) {
            println!("{:<8} {:<40} {} ({})", method, path, handler, file);
        }
    }
}

fn parse_method_handlers(s: &str) -> Vec<(String, String)> {
    let methods = ["get", "post", "put", "patch", "delete", "head", "options"];
    let mut result = Vec::new();

    let mut remaining = s.trim().to_string();
    for method in &methods {
        let prefix = format!("{method}(");
        while let Some(pos) = remaining.find(&prefix) {
            let after = &remaining[pos + prefix.len()..];
            let end = match after.find(')') {
                Some(e) => e,
                None => break,
            };
            let handler = after[..end].trim().to_string();
            result.push((method.to_uppercase(), handler));
            remaining = remaining[pos + prefix.len() + end + 1..].to_string();
        }
    }

    if result.is_empty() {
        result.push(("*".to_string(), s.trim().to_string()));
    }

    result
}