dynami 0.1.0

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

pub fn ensure_mod_declarations(content: &str, submodules: &[String]) -> String {
    let existing_mods = extract_existing_mods(content);

    // Find which mods need to be added
    let mut to_add = Vec::new();
    for submod in submodules {
        if !existing_mods.contains(submod) {
            to_add.push(submod.clone());
        }
    }

    if to_add.is_empty() {
        return content.to_string();
    }

    // Generate mod declarations
    let mod_decls = to_add
        .iter()
        .map(|name| format!("pub mod {};", name))
        .collect::<Vec<_>>()
        .join("\n");

    // Insert before BEGIN marker if it exists, otherwise at top
    if content.contains("// BEGIN DYNAMI GENERATED") {
        let parts: Vec<&str> = content.split("// BEGIN DYNAMI GENERATED").collect();
        if parts.len() >= 2 {
            format!(
                "{}\n{}\n\n// BEGIN DYNAMI GENERATED{}",
                parts[0].trim_end(),
                mod_decls,
                parts[1]
            )
        } else {
            content.to_string()
        }
    } else if content.trim().is_empty() {
        mod_decls
    } else {
        format!("{}\n\n{}", mod_decls, content)
    }
}

fn extract_existing_mods(content: &str) -> Vec<String> {
    if let Ok(file) = parse_file(content) {
        file.items
            .iter()
            .filter_map(|item| {
                if let Item::Mod(item_mod) = item {
                    Some(item_mod.ident.to_string())
                } else {
                    None
                }
            })
            .collect()
    } else {
        Vec::new()
    }
}

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

    #[test]
    fn test_extract_existing_mods() {
        let content = r#"
            pub mod api;
            pub mod users;

            struct Something {}
        "#;

        let mods = extract_existing_mods(content);
        assert_eq!(mods.len(), 2);
        assert!(mods.contains(&"api".to_string()));
        assert!(mods.contains(&"users".to_string()));
    }

    #[test]
    fn test_ensure_mod_declarations_no_duplicates() {
        let content = "pub mod api;\n";
        let submodules = vec!["api".to_string(), "users".to_string()];

        let result = ensure_mod_declarations(content, &submodules);

        // Should only add users, not api (already exists)
        assert!(result.contains("pub mod users;"));
        assert_eq!(result.matches("pub mod api;").count(), 1);
    }

    #[test]
    fn test_ensure_mod_declarations_with_markers() {
        let content = r#"pub mod existing;

// BEGIN DYNAMI GENERATED
pub fn router() -> Router {
    Router::new()
}
// END DYNAMI GENERATED"#;

        let submodules = vec!["api".to_string()];
        let result = ensure_mod_declarations(content, &submodules);

        assert!(result.contains("pub mod api;"));
        assert!(result.contains("pub mod existing;"));
        assert!(result.contains("// BEGIN DYNAMI GENERATED"));

        // api should be before BEGIN marker
        let api_pos = result.find("pub mod api;").unwrap();
        let begin_pos = result.find("// BEGIN DYNAMI GENERATED").unwrap();
        assert!(api_pos < begin_pos);
    }

    #[test]
    fn test_ensure_mod_declarations_empty_content() {
        let content = "";
        let submodules = vec!["api".to_string(), "users".to_string()];

        let result = ensure_mod_declarations(content, &submodules);

        assert!(result.contains("pub mod api;"));
        assert!(result.contains("pub mod users;"));
    }
}