dynami 0.1.0

Automatic Axum router generation from directory structure with file-system based routing
Documentation
mod appstate;
mod generator;
mod handler;
mod import_analyzer;
mod marker;
mod method_analyzer;
mod modgen;
mod parse_utils;
mod parser;
mod route_detector;
mod walker;

/// A marker macro for generated code blocks.
/// This macro simply returns its input unchanged, allowing the code to compile normally.
/// The dynami build tool looks for this macro to identify where to inject generated routes.
#[macro_export]
macro_rules! generate {
    ($($tt:tt)*) => {
        $($tt)*
    };
}

use anyhow::Result;
use std::fs;
use std::path::Path;

/// Main entry point for the route generation library.
///
/// Scans the directory structure starting from `path` and generates
/// router() functions in mod.rs files based on the folder structure
/// and method files found.
///
/// # Arguments
///
/// * `path` - Root path to scan for routes (e.g., "./src/routes")
///
/// # Returns
///
/// Returns Ok(()) on success, or an error if generation fails
///
/// # Example
///
/// ```no_run
/// use dynami::format_routes;
///
/// fn main() {
///     format_routes("./src/routes").unwrap();
/// }
/// ```
pub fn format_routes(path: &str) -> Result<()> {
    format_routes_internal(path, None)
}

/// Debug version of format_routes that writes detailed logs to a file.
///
/// # Arguments
///
/// * `path` - Root path to scan for routes (e.g., "./src/routes")
/// * `log_path` - Path to write debug logs to
///
/// # Example
///
/// ```no_run
/// use dynami::format_routes_with_debug;
///
/// fn main() {
///     format_routes_with_debug("./src/routes", "./dynami-debug.log").unwrap();
/// }
/// ```
pub fn format_routes_with_debug(path: &str, log_path: &str) -> Result<()> {
    format_routes_internal(path, Some(log_path))
}

fn format_routes_internal(path: &str, log_path: Option<&str>) -> Result<()> {
    let mut log = String::new();

    log.push_str(&format!("=== Dynami Debug Log ===\n"));
    log.push_str(&format!("Input path: {}\n", path));

    let root = Path::new(path);
    log.push_str(&format!("Canonicalized path exists: {}\n", root.exists()));

    // 1. Detect State struct name from root mod.rs
    let mod_rs_path = root.join("mod.rs");
    log.push_str(&format!("Looking for mod.rs at: {:?}\n", mod_rs_path));
    log.push_str(&format!("mod.rs exists: {}\n", mod_rs_path.exists()));

    let state_name = appstate::detect_appstate(&mod_rs_path)?;
    log.push_str(&format!("Detected state name: {:?}\n", state_name));

    // 2. Scan directory structure
    log.push_str(&format!("\n=== Scanning directory structure ===\n"));
    let route_tree = walker::scan_routes(root)?;
    log_route_tree(&route_tree, 0, &mut log);

    // 3. Process each node (bottom-up)
    log.push_str(&format!("\n=== Processing nodes ===\n"));
    process_node_with_debug(&route_tree, state_name.as_deref(), &mut log)?;

    log.push_str(&format!("\n=== Done ===\n"));

    // Write log if path provided
    if let Some(log_file) = log_path {
        fs::write(log_file, &log)?;
    }

    Ok(())
}

fn log_route_tree(node: &walker::RouteNode, depth: usize, log: &mut String) {
    let indent = "  ".repeat(depth);
    log.push_str(&format!("{}Path: {:?}\n", indent, node.path));
    log.push_str(&format!(
        "{}Route segment: {}\n",
        indent, node.route_segment
    ));
    log.push_str(&format!(
        "{}Method files: {:?}\n",
        indent, node.method_files
    ));
    log.push_str(&format!("{}Has mod.rs: {}\n", indent, node.has_mod_rs));
    log.push_str(&format!(
        "{}Children count: {}\n",
        indent,
        node.children.len()
    ));

    for child in &node.children {
        log_route_tree(child, depth + 1, log);
    }
}

fn process_node_with_debug(
    node: &walker::RouteNode,
    state_name: Option<&str>,
    log: &mut String,
) -> Result<()> {
    log.push_str(&format!("\nProcessing: {:?}\n", node.path));

    // Process children first (bottom-up)
    for child in &node.children {
        process_node_with_debug(child, state_name, log)?;
    }

    // Generate default handlers for empty method files
    for method_file in &node.method_files {
        let file_path = node.path.join(method_file);
        let is_empty = is_empty_or_missing(&file_path)?;
        log.push_str(&format!(
            "  Method file: {:?}, empty/missing: {}\n",
            file_path, is_empty
        ));

        if is_empty {
            let handler = handler::generate_default_handler(state_name);
            log.push_str(&format!("  Generated handler for: {}\n", method_file));
            fs::write(&file_path, &handler)?;
        }
    }

    // Generate/update mod.rs if it has markers or is empty
    let mod_path = node.path.join("mod.rs");
    let existing = fs::read_to_string(&mod_path).unwrap_or_default();

    log.push_str(&format!("  mod.rs path: {:?}\n", mod_path));
    log.push_str(&format!("  mod.rs exists: {}\n", mod_path.exists()));
    log.push_str(&format!("  mod.rs empty: {}\n", existing.trim().is_empty()));
    log.push_str(&format!(
        "  mod.rs has markers: {}\n",
        marker::has_markers(&existing)
    ));

    if existing.trim().is_empty() || marker::has_markers(&existing) {
        // Collect child module names
        let child_mods: Vec<String> = node
            .children
            .iter()
            .filter_map(|child| {
                child
                    .path
                    .file_name()
                    .and_then(|n| n.to_str())
                    .map(|s| s.to_string())
            })
            .collect();

        log.push_str(&format!("  Child modules: {:?}\n", child_mods));

        // Add module declarations
        let with_mods = if !child_mods.is_empty() {
            modgen::ensure_mod_declarations(&existing, &child_mods)
        } else {
            existing.clone()
        };

        log.push_str(&format!(
            "  After adding mod declarations:\n{}\n",
            with_mods
        ));

        let new_content = if !marker::has_markers(&with_mods) {
            // No markers yet - generate full router function
            let generated = generator::generate_router(node, state_name, &with_mods);
            log.push_str(&format!("  Generated router:\n{}\n", generated));

            // Prepend module declarations
            if with_mods.trim().is_empty() {
                generated
            } else {
                format!("{}\n\n{}", with_mods, generated)
            }
        } else {
            // Has markers - generate just mutations and inject into generate block
            let mutations = generator::generate_mutations(node, &with_mods);
            log.push_str(&format!("  Generated mutations:\n{}\n", mutations));

            // Calculate the routing import needed
            let routing_import = generator::calculate_routing_import(&with_mods, &mutations);

            let (before, after) = marker::extract_user_content(&with_mods);
            marker::inject_generated_with_imports(&before, &mutations, &after, routing_import)
        };

        log.push_str(&format!("  Final content:\n{}\n", new_content));
        log.push_str(&format!("  Writing to: {:?}\n", mod_path));
        fs::write(&mod_path, &new_content)?;
    } else {
        log.push_str(&format!("  Skipping - file has content but no markers\n"));
    }

    Ok(())
}

fn is_empty_or_missing(path: &Path) -> Result<bool> {
    if !path.exists() {
        return Ok(true);
    }
    let content = fs::read_to_string(path)?;
    Ok(content.trim().is_empty())
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs;
    use tempfile::TempDir;

    #[test]
    fn test_format_routes_basic() -> Result<()> {
        let temp = TempDir::new()?;
        let routes = temp.path().join("routes");
        fs::create_dir(&routes)?;

        fs::write(routes.join("mod.rs"), "")?;
        fs::write(routes.join("get.rs"), "")?;

        format_routes(routes.to_str().unwrap())?;

        let mod_content = fs::read_to_string(routes.join("mod.rs"))?;
        assert!(mod_content.contains("pub fn router()"));
        assert!(mod_content.contains("let mut router = Router::new();"));

        let get_content = fs::read_to_string(routes.join("get.rs"))?;
        assert!(get_content.contains("pub async fn handler"));

        Ok(())
    }

    #[test]
    fn test_format_routes_with_nested() -> Result<()> {
        let temp = TempDir::new()?;
        let routes = temp.path().join("routes");
        fs::create_dir(&routes)?;

        fs::write(routes.join("mod.rs"), "")?;
        fs::write(routes.join("get.rs"), "")?;

        let api = routes.join("api");
        fs::create_dir(&api)?;
        fs::write(api.join("mod.rs"), "")?;
        fs::write(api.join("get.rs"), "")?;

        format_routes(routes.to_str().unwrap())?;

        let mod_content = fs::read_to_string(routes.join("mod.rs"))?;
        assert!(mod_content.contains("pub mod api;"));
        assert!(mod_content.contains("router = router.nest(\"/api\", api::router());"));

        Ok(())
    }
}