dynami 0.1.0

Automatic Axum router generation from directory structure with file-system based routing
Documentation
use crate::parse_utils;
use std::collections::HashSet;
use syn::{Expr, ExprMethodCall, Item, Stmt, parse_file};

/// Detects existing routes defined outside the //#dynami::generate block
pub fn detect_existing_routes(content: &str) -> HashSet<(String, String)> {
    let mut routes = HashSet::new();

    // First, find and extract the generated block to skip it
    let user_code = extract_user_code(content);

    // Parse the user code only (not the generated block)
    if let Ok(file) = parse_file(&user_code) {
        for item in &file.items {
            if let Item::Fn(func) = item {
                // Look for routes defined in the function body
                extract_routes_from_function(&func.block.stmts, &mut routes);
            }
        }
    }

    routes
}

/// Extract only user code, excluding the dynami::generate! block
fn extract_user_code(content: &str) -> String {
    if let Some(marker_pos) = content.find("dynami::generate!") {
        let after_marker = marker_pos + "dynami::generate!".len();

        if let Some((_, _open_pos, close_pos)) =
            parse_utils::extract_braced_content(content, after_marker)
        {
            // Combine the code before and after the generated block
            let before = &content[..marker_pos];
            let after = if close_pos + 1 < content.len() {
                &content[close_pos + 1..]
            } else {
                ""
            };

            return format!("{}{}", before, after);
        }
    }

    // No marker found, return entire content
    content.to_string()
}

fn extract_routes_from_function(stmts: &[Stmt], routes: &mut HashSet<(String, String)>) {
    for stmt in stmts {
        match stmt {
            Stmt::Expr(expr, _) => {
                extract_routes_from_expr(expr, routes);
            }
            Stmt::Local(local) => {
                if let Some(init) = &local.init {
                    extract_routes_from_expr(&init.expr, routes);
                }
            }
            _ => {}
        }
    }
}

fn extract_routes_from_expr(expr: &Expr, routes: &mut HashSet<(String, String)>) {
    match expr {
        Expr::Assign(assign) => {
            // Handle router = router.route(...) patterns
            extract_routes_from_expr(&assign.right, routes);
        }
        Expr::MethodCall(method_call) => {
            // Look for .route(...) calls
            if method_call.method == "route" {
                if let Some(route_info) = extract_route_info(method_call) {
                    routes.insert(route_info);
                }
            }
            // Recurse on the receiver
            extract_routes_from_expr(&method_call.receiver, routes);
        }
        Expr::Call(call) => {
            extract_routes_from_expr(&call.func, routes);
        }
        Expr::Block(block) => {
            for stmt in &block.block.stmts {
                if let Stmt::Expr(e, _) = stmt {
                    extract_routes_from_expr(e, routes);
                }
            }
        }
        _ => {}
    }
}

fn extract_route_info(method_call: &ExprMethodCall) -> Option<(String, String)> {
    // Extract path and method from .route(path, method(handler))
    if method_call.args.len() >= 2 {
        // First arg is the path
        if let Some(path) = extract_string_literal(&method_call.args[0]) {
            // Second arg is the method handler
            if let Some(method) = extract_method_name(&method_call.args[1]) {
                return Some((path, method));
            }
        }
    }
    None
}

fn extract_string_literal(expr: &Expr) -> Option<String> {
    if let Expr::Lit(expr_lit) = expr {
        if let syn::Lit::Str(lit_str) = &expr_lit.lit {
            return Some(lit_str.value());
        }
    }
    None
}

fn extract_method_name(expr: &Expr) -> Option<String> {
    // Look for get(handler), post(handler), etc.
    if let Expr::Call(call) = expr {
        if let Expr::Path(path) = &*call.func {
            if let Some(ident) = path.path.get_ident() {
                return Some(ident.to_string());
            }
        }
    }

    // Look for chained methods like get(handler).post(handler2)
    if let Expr::MethodCall(method_call) = expr {
        return Some(method_call.method.to_string());
    }

    None
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_detect_no_routes() {
        let content = r#"
pub fn router() -> Router {
    Router::new()
}
"#;
        let routes = detect_existing_routes(content);
        assert_eq!(routes.len(), 0);
    }

    #[test]
    fn test_detect_single_route() {
        let content = r#"
use axum::routing::get;

pub fn router() -> Router {
    let mut router = Router::new();
    router = router.route("/custom", get(custom_handler));
    router
}
"#;
        let routes = detect_existing_routes(content);
        assert!(routes.contains(&("/custom".to_string(), "get".to_string())));
    }

    #[test]
    fn test_ignore_generated_routes() {
        let content = r#"
pub fn router() -> Router {
    let mut router = Router::new();
    router = router.route("/custom", get(custom_handler));

    dynami::generate! {
        router = router.route("/", get(get::handler))
    }

    router
}
"#;
        let routes = detect_existing_routes(content);
        assert_eq!(routes.len(), 1);
        assert!(routes.contains(&("/custom".to_string(), "get".to_string())));
        assert!(!routes.contains(&("/".to_string(), "get".to_string())));
    }

    #[test]
    fn test_detect_multiple_methods_same_path() {
        let content = r#"
pub fn router() -> Router {
    Router::new()
        .route("/api", get(get_handler).post(post_handler))
}
"#;
        let routes = detect_existing_routes(content);
        // Should detect both get and post for /api
        assert!(routes.len() >= 1);
    }
}