dynami 0.1.0

Automatic Axum router generation from directory structure with file-system based routing
Documentation
use anyhow::{Context, Result};
use std::fs;
use std::path::{Path, PathBuf};

use crate::parser;

#[derive(Debug, Clone)]
pub struct RouteNode {
    pub path: PathBuf,
    pub route_segment: String,
    pub method_files: Vec<String>,
    #[allow(dead_code)]
    pub has_mod_rs: bool,
    pub children: Vec<RouteNode>,
}

pub fn scan_routes(root: &Path) -> Result<RouteNode> {
    let root = root
        .canonicalize()
        .with_context(|| format!("Failed to canonicalize path: {}", root.display()))?;

    scan_directory(&root, true)
}

fn scan_directory(dir: &Path, is_root: bool) -> Result<RouteNode> {
    let folder_name = if is_root {
        ""
    } else {
        dir.file_name().and_then(|n| n.to_str()).unwrap_or("")
    };

    let route_segment = if is_root {
        "/".to_string()
    } else {
        parser::parse_route_segment(folder_name)
    };

    // Check for mod.rs
    let mod_rs_path = dir.join("mod.rs");
    let has_mod_rs = mod_rs_path.exists();

    // Scan for method files
    let mut method_files = Vec::new();
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            if let Ok(file_type) = entry.file_type() {
                if file_type.is_file() {
                    if let Some(name) = entry.file_name().to_str() {
                        if parser::extract_method_from_filename(name).is_some() {
                            method_files.push(name.to_string());
                        }
                    }
                }
            }
        }
    }

    // Scan for subdirectories with mod.rs
    let mut children = Vec::new();
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            if let Ok(file_type) = entry.file_type() {
                if file_type.is_dir() {
                    let subdir = entry.path();
                    // Only include subdirectories that have mod.rs or are empty (will get mod.rs)
                    if subdir.join("mod.rs").exists() || is_empty_or_will_need_modrs(&subdir)? {
                        if let Ok(child_node) = scan_directory(&subdir, false) {
                            children.push(child_node);
                        }
                    }
                }
            }
        }
    }

    Ok(RouteNode {
        path: dir.to_path_buf(),
        route_segment,
        method_files,
        has_mod_rs,
        children,
    })
}

fn is_empty_or_will_need_modrs(dir: &Path) -> Result<bool> {
    // Check if directory has method files or subdirectories
    // If it does, it will need a mod.rs
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            if let Ok(file_type) = entry.file_type() {
                if file_type.is_file() {
                    if let Some(name) = entry.file_name().to_str() {
                        if parser::extract_method_from_filename(name).is_some() {
                            return Ok(true);
                        }
                    }
                } else if file_type.is_dir() {
                    // Has subdirectories
                    return Ok(true);
                }
            }
        }
    }
    Ok(false)
}

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

    #[test]
    fn test_scan_simple_structure() -> Result<()> {
        let temp = TempDir::new()?;
        let root = temp.path();

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

        let node = scan_routes(root)?;

        assert_eq!(node.route_segment, "/");
        assert!(node.has_mod_rs);
        assert_eq!(node.method_files.len(), 2);
        assert!(node.method_files.contains(&"get.rs".to_string()));
        assert!(node.method_files.contains(&"post.rs".to_string()));

        Ok(())
    }

    #[test]
    fn test_scan_nested_structure() -> Result<()> {
        let temp = TempDir::new()?;
        let root = temp.path();

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

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

        let node = scan_routes(root)?;

        assert_eq!(node.children.len(), 1);
        assert_eq!(node.children[0].route_segment, "/api");

        Ok(())
    }
}