dynami 0.1.0

Automatic Axum router generation from directory structure with file-system based routing
Documentation
use crate::import_analyzer;
use crate::parse_utils;

pub fn has_markers(content: &str) -> bool {
    // Check for dynami::generate! macro
    content.contains("dynami::generate!")
}

pub fn extract_user_content(content: &str) -> (String, String) {
    // Find the dynami::generate! macro invocation
    if let Some(marker_pos) = content.find("dynami::generate!") {
        // Find the opening brace after the marker
        let after_marker = marker_pos + "dynami::generate!".len();

        if let Some((_, _open_pos, close_pos)) =
            parse_utils::extract_braced_content(content, after_marker)
        {
            // Extract content before the marker
            let before = content[..marker_pos].trim_end().to_string();

            // Extract content after the closing brace (preserve indentation)
            let after = if close_pos + 1 < content.len() {
                let after_content = &content[close_pos + 1..];
                // Only skip the first newline if present, preserve indentation
                if after_content.starts_with('\n') {
                    after_content[1..].to_string()
                } else if after_content.starts_with("\r\n") {
                    after_content[2..].to_string()
                } else {
                    after_content.to_string()
                }
            } else {
                String::new()
            };

            return (before, after);
        }
    }

    // No markers found, return entire content as "before"
    (content.to_string(), String::new())
}

pub fn inject_generated_with_imports(
    before: &str,
    generated: &str,
    after: &str,
    routing_import: Option<String>,
) -> String {
    let mut result = String::new();

    if !before.is_empty() {
        // Remove old routing imports from before section
        let cleaned_before = import_analyzer::remove_routing_imports(before);

        // Find where to inject the new import (after other imports, before function definitions)
        if let Some(import) = &routing_import {
            let injected = inject_import_in_proper_location(&cleaned_before, import);
            result.push_str(&injected);
        } else {
            result.push_str(&cleaned_before);
        }
        result.push('\n');
    } else if let Some(import) = &routing_import {
        // No before content, just add the import
        result.push_str(import);
        result.push('\n');
        result.push('\n');
    }

    result.push_str("    dynami::generate! {\n");

    // Add generated content with proper indentation
    if !generated.is_empty() {
        result.push_str(generated);
        result.push('\n');
    }

    result.push_str("    }");

    if !after.is_empty() {
        result.push('\n');
        result.push_str(after);
    }

    result
}

/// Inject the import statement in the proper location (after existing imports, before functions)
fn inject_import_in_proper_location(content: &str, import: &str) -> String {
    // Check if the import already exists in the content
    if content.contains(import) {
        return content.to_string();
    }

    let lines: Vec<&str> = content.lines().collect();
    let mut result = Vec::new();
    let mut import_inserted = false;
    let mut last_import_index = None;

    // Find the last import statement
    for (i, line) in lines.iter().enumerate() {
        let trimmed = line.trim();
        if trimmed.starts_with("use ") || trimmed.starts_with("pub use ") {
            last_import_index = Some(i);
        }
    }

    // Insert the import after the last import or at the beginning
    for (i, line) in lines.iter().enumerate() {
        result.push(line.to_string());

        if !import_inserted {
            if let Some(last_idx) = last_import_index {
                // Insert after the last import
                if i == last_idx {
                    result.push(import.to_string());
                    import_inserted = true;
                }
            } else if i == 0 {
                // No imports found, insert at beginning after first line
                result.push(String::new());
                result.push(import.to_string());
                import_inserted = true;
            }
        }
    }

    // If we still haven't inserted (shouldn't happen), append at end
    if !import_inserted {
        result.push(String::new());
        result.push(import.to_string());
    }

    result.join("\n")
}

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

    #[test]
    fn test_has_markers() {
        let content = r#"
pub fn router() -> Router {
    let mut router = Router::new();
    dynami::generate! {
        router = router.route("/", get(get::handler))
    }
    router
}
"#;
        assert!(has_markers(content));

        let content_no_markers = "some code";
        assert!(!has_markers(content_no_markers));
    }

    #[test]
    fn test_extract_user_content() {
        let content = r#"use axum::Router;

pub fn router() -> Router {
    let mut router = Router::new();
    dynami::generate! {
        router = router.route("/", get(get::handler))
    }
    router
}"#;
        let (before, after) = extract_user_content(content);
        assert!(before.contains("let mut router = Router::new();"));
        assert!(after.contains("router"));
        assert!(!after.contains("dynami::generate!"));
    }

    #[test]
    fn test_inject_generated() {
        let before = r#"pub fn router() -> Router {
    let mut router = Router::new();"#;
        let generated = "        router = router.route(\"/\", get(get::handler));";
        let after = "    router\n}";

        let result = inject_generated_with_imports(before, generated, after, None);
        assert!(result.contains("dynami::generate!"));
        assert!(result.contains("router = router.route"));
        assert!(result.contains("let mut router"));
    }

    #[test]
    fn test_extract_nested_braces() {
        let content = r#"
pub fn router() -> Router {
    let mut router = Router::new();
    dynami::generate! {
        router = router.route("/", get(|| async { "nested" }));
    }
    router
}
"#;
        let (before, after) = extract_user_content(content);
        assert!(before.contains("let mut router"));
        assert!(after.trim().starts_with("router"));
    }
}