dynami 0.1.0

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

/// Analyzes existing imports in the code and extracts axum::routing methods
pub fn extract_existing_routing_imports(content: &str) -> HashSet<String> {
    let mut methods = HashSet::new();

    if let Ok(file) = parse_file(content) {
        for item in &file.items {
            if let Item::Use(use_item) = item {
                extract_from_use_tree(&use_item.tree, &[], &mut methods);
            }
        }
    }

    methods
}

/// Recursively extract routing methods from use tree
fn extract_from_use_tree(tree: &UseTree, path: &[String], methods: &mut HashSet<String>) {
    match tree {
        UseTree::Path(use_path) => {
            let mut new_path = path.to_vec();
            new_path.push(use_path.ident.to_string());
            extract_from_use_tree(&use_path.tree, &new_path, methods);
        }
        UseTree::Name(use_name) => {
            // Check if we're in axum::routing path
            if is_routing_path(path) {
                methods.insert(use_name.ident.to_string());
            }
        }
        UseTree::Group(use_group) => {
            for item in &use_group.items {
                extract_from_use_tree(item, path, methods);
            }
        }
        UseTree::Rename(use_rename) => {
            // We don't handle renamed imports for routing methods
            if is_routing_path(path) {
                methods.insert(use_rename.ident.to_string());
            }
        }
        _ => {}
    }
}

/// Check if the path represents axum::routing
fn is_routing_path(path: &[String]) -> bool {
    path.len() >= 2 && path[0] == "axum" && path[1] == "routing"
}

/// Generate the simplified import statement
/// Always uses the form: use axum::routing::{method1, method2};
pub fn generate_routing_import(methods: &HashSet<String>) -> Option<String> {
    if methods.is_empty() {
        return None;
    }

    let mut sorted_methods: Vec<_> = methods.iter().collect();
    sorted_methods.sort();

    if sorted_methods.len() == 1 {
        Some(format!("use axum::routing::{{{}}};", sorted_methods[0]))
    } else {
        Some(format!(
            "use axum::routing::{{{}}};",
            sorted_methods
                .iter()
                .map(|s| s.as_str())
                .collect::<Vec<_>>()
                .join(", ")
        ))
    }
}

/// Remove only the routing imports from axum use statements, preserving everything else
pub fn remove_routing_imports(content: &str) -> String {
    let lines: Vec<&str> = content.lines().collect();
    let mut result = Vec::new();

    for line in lines {
        let trimmed = line.trim();

        // Check if this is a use statement with axum routing
        if trimmed.starts_with("use axum::routing")
            || trimmed.starts_with("pub use axum::routing") 
        {
            // This is a pure routing import, skip it entirely
            continue;
        } else if trimmed.starts_with("use axum::") || trimmed.starts_with("use axum::{") {
            // This might have routing mixed with other imports
            if trimmed.contains("routing::") || trimmed.contains("routing::{") {
                let cleaned = remove_routing_patterns(line);
                // Only add if there's still content after removal
                if !cleaned.trim().is_empty()
                    && !cleaned.trim().ends_with("use axum::{};")
                    && !cleaned.trim().ends_with("use axum::{}")
                    && cleaned.trim() != "use axum::{};"
                {
                    result.push(cleaned);
                }
            } else {
                // No routing in this axum import
                result.push(line.to_string());
            }
        } else {
            // Not an axum import
            result.push(line.to_string());
        }
    }

    result.join("\n")
}

/// Remove routing patterns from a line while preserving formatting
fn remove_routing_patterns(line: &str) -> String {
    let mut result = line.to_string();

    // List of HTTP methods
    let methods = ["get", "post", "put", "delete", "patch", "options", "head"];

    // Remove patterns for each method
    for method in &methods {
        // Pattern: routing::method
        let pattern1 = format!("routing::{}", method);
        result = result.replace(&format!(", {}", pattern1), "");
        result = result.replace(&format!("{}, ", pattern1), "");
        result = result.replace(&pattern1, "");

        // Pattern: routing::{method}
        let pattern2 = format!("routing::{{{}}}", method);
        result = result.replace(&format!(", {}", pattern2), "");
        result = result.replace(&format!("{}, ", pattern2), "");
        result = result.replace(&pattern2, "");

        // Pattern: routing::{method, ...} or routing::{..., method}
        let pattern3_start = format!("routing::{{{}", method);
        let pattern3_end = format!("{}}}", method);
        result = result.replace(&format!(", {}", pattern3_start), "");
        result = result.replace(&format!("{}, ", pattern3_start), "");
        result = result.replace(&format!(", {}", pattern3_end), "");
        result = result.replace(&format!("{}, ", pattern3_end), "");
    }

    // Clean up any formatting issues
    result = result.replace(",,", ",");
    result = result.replace(", ,", ",");
    result = result.replace("{,", "{");
    result = result.replace(",}", "}");
    result = result.replace(", }", "}");
    result = result.replace("{ ,", "{");

    result
}

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

    #[test]
    fn test_extract_simple_import() {
        let code = r#"use axum::routing::get;"#;
        let methods = extract_existing_routing_imports(code);
        assert!(methods.contains("get"));
        assert_eq!(methods.len(), 1);
    }

    #[test]
    fn test_extract_grouped_import() {
        let code = r#"use axum::routing::{get, post};"#;
        let methods = extract_existing_routing_imports(code);
        assert!(methods.contains("get"));
        assert!(methods.contains("post"));
        assert_eq!(methods.len(), 2);
    }

    #[test]
    fn test_extract_nested_import() {
        let code = r#"use axum::{Router, routing::{get, post}};"#;
        let methods = extract_existing_routing_imports(code);
        assert!(methods.contains("get"));
        assert!(methods.contains("post"));
        assert_eq!(methods.len(), 2);
    }

    #[test]
    fn test_extract_deeply_nested() {
        let code = r#"use axum::{something, routing::get};"#;
        let methods = extract_existing_routing_imports(code);
        assert!(methods.contains("get"));
        assert_eq!(methods.len(), 1);
    }

    #[test]
    fn test_generate_single_import() {
        let mut methods = HashSet::new();
        methods.insert("get".to_string());

        let import = generate_routing_import(&methods).unwrap();
        assert_eq!(import, "use axum::routing::{get};");
    }

    #[test]
    fn test_generate_multiple_imports_sorted() {
        let mut methods = HashSet::new();
        methods.insert("post".to_string());
        methods.insert("get".to_string());
        methods.insert("delete".to_string());

        let import = generate_routing_import(&methods).unwrap();
        assert_eq!(import, "use axum::routing::{delete, get, post};");
    }

    #[test]
    fn test_remove_simple_routing_import() {
        let code = r#"use axum::Router;
use axum::routing::{get, post};

pub fn router() -> Router {}"#;

        let result = remove_routing_imports(code);
        assert!(!result.contains("routing"));
        assert!(result.contains("use axum::Router"));
    }

    #[test]
    fn test_remove_routing_keep_others() {
        let code = r#"use axum::{http::StatusCode, response::IntoResponse, routing::get, Router};"#;

        let result = remove_routing_imports(code);
        assert!(!result.contains("routing"));
        assert!(result.contains("http::StatusCode"));
        assert!(result.contains("response::IntoResponse"));
        assert!(result.contains("Router"));
    }

    #[test]
    fn test_no_imports() {
        let code = r#"use axum::Router;"#;
        let methods = extract_existing_routing_imports(code);
        assert_eq!(methods.len(), 0);
    }
}