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;

/// Analyzes route code to extract HTTP methods that need to be imported
/// Only the first method in a chain needs to be imported: get(handler).post(handler2)
/// Here, only `get` needs to be imported; `post` is called as a method on MethodRouter
pub fn extract_http_methods(code: &str) -> HashSet<String> {
    let mut methods = HashSet::new();

    // HTTP methods we're looking for
    let http_methods = ["get", "post", "put", "delete", "patch", "options", "head"];

    // Search for .route() calls and analyze what's inside
    let mut pos = 0;
    while let Some(route_pos) = code[pos..].find(".route(") {
        let abs_pos = pos + route_pos;

        // Extract the content inside .route(...)
        if let Some((route_args, _, close_pos)) =
            parse_utils::extract_parenthesized_content(code, abs_pos)
        {
            // Find the first HTTP method call (not preceded by a dot)
            // Pattern: method(...) but not .method(...)
            if let Some(first_method) = find_first_method_in_chain(&route_args, &http_methods) {
                methods.insert(first_method);
            }

            pos = close_pos + 1;
        } else {
            break;
        }
    }

    methods
}

/// Finds the first HTTP method in a chain (the one that needs to be imported)
/// For example, in "get(get::handler).post(post::handler)", returns "get"
fn find_first_method_in_chain(content: &str, http_methods: &[&str]) -> Option<String> {
    // Skip the path parameter (first argument to .route())
    // Find the comma that separates path from handler
    let comma_pos = content.find(',')?;
    let handler_part = &content[comma_pos + 1..];

    // Find the first method call that is NOT preceded by a dot
    let mut min_pos = handler_part.len();
    let mut found_method = None;

    for method in http_methods {
        let pattern = format!("{}(", method);

        // Find all occurrences of this method
        let mut search_pos = 0;
        while let Some(pos) = handler_part[search_pos..].find(&pattern) {
            let abs_pos = search_pos + pos;

            // Check if this is the start or preceded by a non-dot character
            let is_first_in_chain = abs_pos == 0 || {
                let prev_chars = &handler_part[..abs_pos].trim_end();
                !prev_chars.ends_with('.')
            };

            if is_first_in_chain && abs_pos < min_pos {
                min_pos = abs_pos;
                found_method = Some(method.to_string());
            }

            search_pos = abs_pos + 1;
        }
    }

    found_method
}

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

    #[test]
    fn test_extract_single_method() {
        let code = r#"router = router.route("/", get(get::handler));"#;
        let methods = extract_http_methods(code);
        assert!(methods.contains("get"));
        assert_eq!(methods.len(), 1);
    }

    #[test]
    fn test_extract_chained_methods_only_first() {
        let code = r#"router = router.route("/", get(get::handler).post(post::handler));"#;
        let methods = extract_http_methods(code);
        assert!(methods.contains("get"));
        assert!(!methods.contains("post")); // post is chained, should not be imported
        assert_eq!(methods.len(), 1);
    }

    #[test]
    fn test_extract_multiple_routes_different_first_methods() {
        let code = r#"
            router = router.route("/", get(get::handler).post(post::handler));
            router = router.route("/api", put(put::handler).delete(delete::handler));
            router = router.route("/patch", patch(patch::handler));
        "#;
        let methods = extract_http_methods(code);
        assert!(methods.contains("get"));
        assert!(!methods.contains("post")); // chained
        assert!(methods.contains("put"));
        assert!(!methods.contains("delete")); // chained
        assert!(methods.contains("patch"));
        assert_eq!(methods.len(), 3); // get, put, patch
    }

    #[test]
    fn test_no_methods() {
        let code = r#"router = router.nest("/api", api::router());"#;
        let methods = extract_http_methods(code);
        assert_eq!(methods.len(), 0);
    }

    #[test]
    fn test_multiple_different_routes() {
        let code = r#"
            router = router.route("/one", get(handler1));
            router = router.route("/two", post(handler2));
        "#;
        let methods = extract_http_methods(code);
        assert!(methods.contains("get"));
        assert!(methods.contains("post"));
        assert_eq!(methods.len(), 2);
    }
}