dynami 0.1.0

Automatic Axum router generation from directory structure with file-system based routing
Documentation
use crate::import_analyzer;
use crate::method_analyzer;
use crate::parser;
use crate::route_detector;
use crate::walker::RouteNode;
use std::collections::HashSet;

pub fn generate_router(
    node: &RouteNode,
    state_name: Option<&str>,
    existing_content: &str,
) -> String {
    let mut mod_declarations = Vec::new();
    let mut imports = vec!["use axum::Router;".to_string()];

    // Collect module declarations for method files
    for method_file in &node.method_files {
        if parser::extract_method_from_filename(method_file).is_some() {
            let module_name = method_file.strip_suffix(".rs").unwrap_or(method_file);
            mod_declarations.push(format!("pub mod {};", module_name));
        }
    }

    // Generate router type
    let router_type = if let Some(state) = state_name {
        format!("Router<{}>", state)
    } else {
        "Router".to_string()
    };

    // Generate router mutations
    let mutations = generate_mutations_internal(node, existing_content);

    // Extract existing routing imports from the content
    let existing_methods = import_analyzer::extract_existing_routing_imports(existing_content);

    // Analyze the generated mutations to detect which HTTP methods are needed
    let needed_methods = method_analyzer::extract_http_methods(&mutations);

    // Merge existing and needed methods
    let mut all_methods: HashSet<String> = existing_methods;
    all_methods.extend(needed_methods);

    // Generate the simplified routing import
    if let Some(routing_import) = import_analyzer::generate_routing_import(&all_methods) {
        imports.push(routing_import);
    }

    // Build final code
    let mod_decls_str = if mod_declarations.is_empty() {
        String::new()
    } else {
        mod_declarations.join("\n")
    };

    // Remove old routing imports and add Router import if needed
    let cleaned_imports = if existing_content.contains("use axum::Router") {
        imports
            .iter()
            .skip(1)
            .cloned()
            .collect::<Vec<_>>()
            .join("\n")
    } else {
        imports.join("\n")
    };

    let imports_str = cleaned_imports;

    // Wrap mutations in dynami::generate! block
    let generate_block = if mutations.is_empty() {
        "    dynami::generate! {\n    }".to_string()
    } else {
        format!("    dynami::generate! {{\n{}\n    }}", mutations)
    };

    format!(
        r#"{}

{}

pub fn router() -> {} {{
    let mut router = Router::new();
{}
    router
}}
"#,
        mod_decls_str, imports_str, router_type, generate_block
    )
}

/// Generate just the mutation code for //#dynami::generate block
pub fn generate_mutations(node: &RouteNode, existing_content: &str) -> String {
    generate_mutations_internal(node, existing_content)
}

/// Calculate the routing import needed based on existing content and mutations
pub fn calculate_routing_import(existing_content: &str, mutations: &str) -> Option<String> {
    // Extract existing routing imports from the content
    let existing_methods = import_analyzer::extract_existing_routing_imports(existing_content);

    // Analyze the mutations to detect which HTTP methods are needed
    let needed_methods = method_analyzer::extract_http_methods(mutations);

    // Merge existing and needed methods
    let mut all_methods: HashSet<String> = existing_methods;
    all_methods.extend(needed_methods);

    // Generate the simplified routing import
    import_analyzer::generate_routing_import(&all_methods)
}

fn generate_mutations_internal(node: &RouteNode, existing_content: &str) -> String {
    // Detect existing routes to avoid duplication
    let existing_routes = route_detector::detect_existing_routes(existing_content);

    let mut router_mutations = Vec::new();

    // Add method routes (only if not already defined by user)
    if !node.method_files.is_empty() {
        let mut method_handlers = Vec::new();
        for method_file in &node.method_files {
            if let Some(method) = parser::extract_method_from_filename(method_file) {
                // Check if user already defined this route + method combination
                if !existing_routes.contains(&("/".to_string(), method.to_string())) {
                    let module_name = method_file.strip_suffix(".rs").unwrap_or(method_file);
                    method_handlers.push(format!("{}({}::handler)", method, module_name));
                }
            }
        }
        if !method_handlers.is_empty() {
            router_mutations.push(format!(
                "        router = router.route(\"/\", {});",
                method_handlers.join(".")
            ));
        }
    }

    // Add nested routes
    for child in &node.children {
        let child_name = child
            .path
            .file_name()
            .and_then(|n| n.to_str())
            .unwrap_or("unknown");
        router_mutations.push(format!(
            "        router = router.nest(\"{}\", {}::router());",
            child.route_segment, child_name
        ));
    }

    router_mutations.join("\n")
}

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

    #[test]
    fn test_generate_router_simple() {
        let node = RouteNode {
            path: PathBuf::from("/routes"),
            route_segment: "/".to_string(),
            method_files: vec!["get.rs".to_string(), "post.rs".to_string()],
            has_mod_rs: true,
            children: vec![],
        };

        let result = generate_router(&node, None, "");

        assert!(result.contains("use axum::Router;"));
        assert!(result.contains("use axum::routing::"));
        assert!(result.contains("pub fn router() -> Router"));
        assert!(result.contains("let mut router = Router::new();"));
        assert!(result.contains("dynami::generate!"));
        assert!(
            result.contains("router = router.route(\"/\", get(get::handler).post(post::handler));")
        );
    }

    #[test]
    fn test_generate_router_with_state() {
        let node = RouteNode {
            path: PathBuf::from("/routes"),
            route_segment: "/".to_string(),
            method_files: vec!["get.rs".to_string()],
            has_mod_rs: true,
            children: vec![],
        };

        let result = generate_router(&node, Some("AppState"), "");

        assert!(result.contains("pub fn router() -> Router<AppState>"));
    }

    #[test]
    fn test_generate_router_with_nested() {
        let child = RouteNode {
            path: PathBuf::from("/routes/api"),
            route_segment: "/api".to_string(),
            method_files: vec![],
            has_mod_rs: true,
            children: vec![],
        };

        let node = RouteNode {
            path: PathBuf::from("/routes"),
            route_segment: "/".to_string(),
            method_files: vec!["get.rs".to_string()],
            has_mod_rs: true,
            children: vec![child],
        };

        let result = generate_router(&node, None, "");

        assert!(result.contains("router = router.nest(\"/api\", api::router());"));
    }

    #[test]
    fn test_generate_router_dynamic_route() {
        let child = RouteNode {
            path: PathBuf::from("/routes/d_id"),
            route_segment: "/{id}".to_string(),
            method_files: vec![],
            has_mod_rs: true,
            children: vec![],
        };

        let node = RouteNode {
            path: PathBuf::from("/routes"),
            route_segment: "/".to_string(),
            method_files: vec![],
            has_mod_rs: true,
            children: vec![child],
        };

        let result = generate_router(&node, None, "");

        assert!(result.contains("router = router.nest(\"/{id}\", d_id::router());"));
    }

    #[test]
    fn test_smart_import_generation() {
        // get and post methods, but post is chained so only get needs to be imported
        let node = RouteNode {
            path: PathBuf::from("/routes"),
            route_segment: "/".to_string(),
            method_files: vec!["get.rs".to_string(), "post.rs".to_string()],
            has_mod_rs: true,
            children: vec![],
        };

        let result = generate_router(&node, None, "");

        // Should only import get (post is chained via .post())
        assert!(result.contains("use axum::routing::{get}"));
        assert!(!result.contains("delete"));
        assert!(!result.contains("put"));
    }

    #[test]
    fn test_no_routing_imports_for_only_nested() {
        // No method files, only nested routes
        let child = RouteNode {
            path: PathBuf::from("/routes/api"),
            route_segment: "/api".to_string(),
            method_files: vec![],
            has_mod_rs: true,
            children: vec![],
        };

        let node = RouteNode {
            path: PathBuf::from("/routes"),
            route_segment: "/".to_string(),
            method_files: vec![],
            has_mod_rs: true,
            children: vec![child],
        };

        let result = generate_router(&node, None, "");

        // Should not have routing imports since no HTTP methods are used
        assert!(!result.contains("use axum::routing::{"));
        assert!(result.contains("use axum::Router;"));
    }
}