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;
#[macro_export]
macro_rules! generate {
($($tt:tt)*) => {
$($tt)*
};
}
use anyhow::Result;
use std::fs;
use std::path::Path;
pub fn format_routes(path: &str) -> Result<()> {
format_routes_internal(path, None)
}
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()));
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));
log.push_str(&format!("\n=== Scanning directory structure ===\n"));
let route_tree = walker::scan_routes(root)?;
log_route_tree(&route_tree, 0, &mut log);
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"));
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));
for child in &node.children {
process_node_with_debug(child, state_name, log)?;
}
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)?;
}
}
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) {
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));
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) {
let generated = generator::generate_router(node, state_name, &with_mods);
log.push_str(&format!(" Generated router:\n{}\n", generated));
if with_mods.trim().is_empty() {
generated
} else {
format!("{}\n\n{}", with_mods, generated)
}
} else {
let mutations = generator::generate_mutations(node, &with_mods);
log.push_str(&format!(" Generated mutations:\n{}\n", mutations));
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(())
}
}