1mod appstate;
2mod generator;
3mod handler;
4mod import_analyzer;
5mod marker;
6mod method_analyzer;
7mod modgen;
8mod parse_utils;
9mod parser;
10mod route_detector;
11mod walker;
12
13#[macro_export]
17macro_rules! generate {
18 ($($tt:tt)*) => {
19 $($tt)*
20 };
21}
22
23use anyhow::Result;
24use std::fs;
25use std::path::Path;
26
27pub fn format_routes(path: &str) -> Result<()> {
51 format_routes_internal(path, None)
52}
53
54pub fn format_routes_with_debug(path: &str, log_path: &str) -> Result<()> {
71 format_routes_internal(path, Some(log_path))
72}
73
74fn format_routes_internal(path: &str, log_path: Option<&str>) -> Result<()> {
75 let mut log = String::new();
76
77 log.push_str(&format!("=== Dynami Debug Log ===\n"));
78 log.push_str(&format!("Input path: {}\n", path));
79
80 let root = Path::new(path);
81 log.push_str(&format!("Canonicalized path exists: {}\n", root.exists()));
82
83 let mod_rs_path = root.join("mod.rs");
85 log.push_str(&format!("Looking for mod.rs at: {:?}\n", mod_rs_path));
86 log.push_str(&format!("mod.rs exists: {}\n", mod_rs_path.exists()));
87
88 let state_name = appstate::detect_appstate(&mod_rs_path)?;
89 log.push_str(&format!("Detected state name: {:?}\n", state_name));
90
91 log.push_str(&format!("\n=== Scanning directory structure ===\n"));
93 let route_tree = walker::scan_routes(root)?;
94 log_route_tree(&route_tree, 0, &mut log);
95
96 log.push_str(&format!("\n=== Processing nodes ===\n"));
98 process_node_with_debug(&route_tree, state_name.as_deref(), &mut log)?;
99
100 log.push_str(&format!("\n=== Done ===\n"));
101
102 if let Some(log_file) = log_path {
104 fs::write(log_file, &log)?;
105 }
106
107 Ok(())
108}
109
110fn log_route_tree(node: &walker::RouteNode, depth: usize, log: &mut String) {
111 let indent = " ".repeat(depth);
112 log.push_str(&format!("{}Path: {:?}\n", indent, node.path));
113 log.push_str(&format!(
114 "{}Route segment: {}\n",
115 indent, node.route_segment
116 ));
117 log.push_str(&format!(
118 "{}Method files: {:?}\n",
119 indent, node.method_files
120 ));
121 log.push_str(&format!("{}Has mod.rs: {}\n", indent, node.has_mod_rs));
122 log.push_str(&format!(
123 "{}Children count: {}\n",
124 indent,
125 node.children.len()
126 ));
127
128 for child in &node.children {
129 log_route_tree(child, depth + 1, log);
130 }
131}
132
133fn process_node_with_debug(
134 node: &walker::RouteNode,
135 state_name: Option<&str>,
136 log: &mut String,
137) -> Result<()> {
138 log.push_str(&format!("\nProcessing: {:?}\n", node.path));
139
140 for child in &node.children {
142 process_node_with_debug(child, state_name, log)?;
143 }
144
145 for method_file in &node.method_files {
147 let file_path = node.path.join(method_file);
148 let is_empty = is_empty_or_missing(&file_path)?;
149 log.push_str(&format!(
150 " Method file: {:?}, empty/missing: {}\n",
151 file_path, is_empty
152 ));
153
154 if is_empty {
155 let handler = handler::generate_default_handler(state_name);
156 log.push_str(&format!(" Generated handler for: {}\n", method_file));
157 fs::write(&file_path, &handler)?;
158 }
159 }
160
161 let mod_path = node.path.join("mod.rs");
163 let existing = fs::read_to_string(&mod_path).unwrap_or_default();
164
165 log.push_str(&format!(" mod.rs path: {:?}\n", mod_path));
166 log.push_str(&format!(" mod.rs exists: {}\n", mod_path.exists()));
167 log.push_str(&format!(" mod.rs empty: {}\n", existing.trim().is_empty()));
168 log.push_str(&format!(
169 " mod.rs has markers: {}\n",
170 marker::has_markers(&existing)
171 ));
172
173 if existing.trim().is_empty() || marker::has_markers(&existing) {
174 let child_mods: Vec<String> = node
176 .children
177 .iter()
178 .filter_map(|child| {
179 child
180 .path
181 .file_name()
182 .and_then(|n| n.to_str())
183 .map(|s| s.to_string())
184 })
185 .collect();
186
187 log.push_str(&format!(" Child modules: {:?}\n", child_mods));
188
189 let with_mods = if !child_mods.is_empty() {
191 modgen::ensure_mod_declarations(&existing, &child_mods)
192 } else {
193 existing.clone()
194 };
195
196 log.push_str(&format!(
197 " After adding mod declarations:\n{}\n",
198 with_mods
199 ));
200
201 let new_content = if !marker::has_markers(&with_mods) {
202 let generated = generator::generate_router(node, state_name, &with_mods);
204 log.push_str(&format!(" Generated router:\n{}\n", generated));
205
206 if with_mods.trim().is_empty() {
208 generated
209 } else {
210 format!("{}\n\n{}", with_mods, generated)
211 }
212 } else {
213 let mutations = generator::generate_mutations(node, &with_mods);
215 log.push_str(&format!(" Generated mutations:\n{}\n", mutations));
216
217 let routing_import = generator::calculate_routing_import(&with_mods, &mutations);
219
220 let (before, after) = marker::extract_user_content(&with_mods);
221 marker::inject_generated_with_imports(&before, &mutations, &after, routing_import)
222 };
223
224 log.push_str(&format!(" Final content:\n{}\n", new_content));
225 log.push_str(&format!(" Writing to: {:?}\n", mod_path));
226 fs::write(&mod_path, &new_content)?;
227 } else {
228 log.push_str(&format!(" Skipping - file has content but no markers\n"));
229 }
230
231 Ok(())
232}
233
234fn is_empty_or_missing(path: &Path) -> Result<bool> {
235 if !path.exists() {
236 return Ok(true);
237 }
238 let content = fs::read_to_string(path)?;
239 Ok(content.trim().is_empty())
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use std::fs;
246 use tempfile::TempDir;
247
248 #[test]
249 fn test_format_routes_basic() -> Result<()> {
250 let temp = TempDir::new()?;
251 let routes = temp.path().join("routes");
252 fs::create_dir(&routes)?;
253
254 fs::write(routes.join("mod.rs"), "")?;
255 fs::write(routes.join("get.rs"), "")?;
256
257 format_routes(routes.to_str().unwrap())?;
258
259 let mod_content = fs::read_to_string(routes.join("mod.rs"))?;
260 assert!(mod_content.contains("pub fn router()"));
261 assert!(mod_content.contains("let mut router = Router::new();"));
262
263 let get_content = fs::read_to_string(routes.join("get.rs"))?;
264 assert!(get_content.contains("pub async fn handler"));
265
266 Ok(())
267 }
268
269 #[test]
270 fn test_format_routes_with_nested() -> Result<()> {
271 let temp = TempDir::new()?;
272 let routes = temp.path().join("routes");
273 fs::create_dir(&routes)?;
274
275 fs::write(routes.join("mod.rs"), "")?;
276 fs::write(routes.join("get.rs"), "")?;
277
278 let api = routes.join("api");
279 fs::create_dir(&api)?;
280 fs::write(api.join("mod.rs"), "")?;
281 fs::write(api.join("get.rs"), "")?;
282
283 format_routes(routes.to_str().unwrap())?;
284
285 let mod_content = fs::read_to_string(routes.join("mod.rs"))?;
286 assert!(mod_content.contains("pub mod api;"));
287 assert!(mod_content.contains("router = router.nest(\"/api\", api::router());"));
288
289 Ok(())
290 }
291}