dynami/
lib.rs

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/// A marker macro for generated code blocks.
14/// This macro simply returns its input unchanged, allowing the code to compile normally.
15/// The dynami build tool looks for this macro to identify where to inject generated routes.
16#[macro_export]
17macro_rules! generate {
18    ($($tt:tt)*) => {
19        $($tt)*
20    };
21}
22
23use anyhow::Result;
24use std::fs;
25use std::path::Path;
26
27/// Main entry point for the route generation library.
28///
29/// Scans the directory structure starting from `path` and generates
30/// router() functions in mod.rs files based on the folder structure
31/// and method files found.
32///
33/// # Arguments
34///
35/// * `path` - Root path to scan for routes (e.g., "./src/routes")
36///
37/// # Returns
38///
39/// Returns Ok(()) on success, or an error if generation fails
40///
41/// # Example
42///
43/// ```no_run
44/// use dynami::format_routes;
45///
46/// fn main() {
47///     format_routes("./src/routes").unwrap();
48/// }
49/// ```
50pub fn format_routes(path: &str) -> Result<()> {
51    format_routes_internal(path, None)
52}
53
54/// Debug version of format_routes that writes detailed logs to a file.
55///
56/// # Arguments
57///
58/// * `path` - Root path to scan for routes (e.g., "./src/routes")
59/// * `log_path` - Path to write debug logs to
60///
61/// # Example
62///
63/// ```no_run
64/// use dynami::format_routes_with_debug;
65///
66/// fn main() {
67///     format_routes_with_debug("./src/routes", "./dynami-debug.log").unwrap();
68/// }
69/// ```
70pub 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    // 1. Detect State struct name from root mod.rs
84    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    // 2. Scan directory structure
92    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    // 3. Process each node (bottom-up)
97    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    // Write log if path provided
103    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    // Process children first (bottom-up)
141    for child in &node.children {
142        process_node_with_debug(child, state_name, log)?;
143    }
144
145    // Generate default handlers for empty method files
146    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    // Generate/update mod.rs if it has markers or is empty
162    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        // Collect child module names
175        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        // Add module declarations
190        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            // No markers yet - generate full router function
203            let generated = generator::generate_router(node, state_name, &with_mods);
204            log.push_str(&format!("  Generated router:\n{}\n", generated));
205
206            // Prepend module declarations
207            if with_mods.trim().is_empty() {
208                generated
209            } else {
210                format!("{}\n\n{}", with_mods, generated)
211            }
212        } else {
213            // Has markers - generate just mutations and inject into generate block
214            let mutations = generator::generate_mutations(node, &with_mods);
215            log.push_str(&format!("  Generated mutations:\n{}\n", mutations));
216
217            // Calculate the routing import needed
218            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}