Skip to main content

ryo_source/generator/
multi.rs

1//! Multi-file generator.
2//!
3//! Generates code into multiple files following Rust module conventions.
4//! Supports both modern (`module.rs` + `module/`) and legacy (`module/mod.rs`) styles.
5
6use super::{GeneratedSource, ModuleTree};
7use crate::pure::{PureFile, PureItem, PureMod};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11/// Result of multi-file generation.
12#[derive(Debug, Clone, Default)]
13pub struct GeneratedFiles {
14    /// Map of relative file paths to generated source.
15    pub files: HashMap<PathBuf, GeneratedSource>,
16}
17
18impl GeneratedFiles {
19    /// Create empty GeneratedFiles.
20    pub fn new() -> Self {
21        Self::default()
22    }
23
24    /// Get all file paths.
25    pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
26        self.files.keys()
27    }
28
29    /// Get a specific file.
30    pub fn get(&self, path: &Path) -> Option<&GeneratedSource> {
31        self.files.get(path)
32    }
33
34    /// Get number of files.
35    pub fn len(&self) -> usize {
36        self.files.len()
37    }
38
39    /// Check if empty.
40    pub fn is_empty(&self) -> bool {
41        self.files.is_empty()
42    }
43
44    /// Compute diff against existing PureFiles.
45    ///
46    /// Returns only the files that have changed (or are new).
47    /// Comparison is done on PureFile structure, not source text,
48    /// so formatting differences are ignored.
49    pub fn diff(&self, existing: &HashMap<PathBuf, PureFile>) -> GeneratedFiles {
50        let mut changed = GeneratedFiles::new();
51
52        for (path, generated) in &self.files {
53            let is_changed = match existing.get(path) {
54                None => true, // New file
55                Some(existing_pure) => {
56                    // Compare PureFile structures
57                    // We use Debug representation for structural comparison
58                    // This is not perfect but works for most cases
59                    format!("{:?}", generated.pure_file) != format!("{:?}", existing_pure)
60                }
61            };
62
63            if is_changed {
64                changed.files.insert(path.clone(), generated.clone());
65            }
66        }
67
68        changed
69    }
70
71    /// Get paths of files that would be deleted (exist in `existing` but not in generated).
72    pub fn deleted_paths<'a>(&self, existing: &'a HashMap<PathBuf, PureFile>) -> Vec<&'a PathBuf> {
73        existing
74            .keys()
75            .filter(|path| !self.files.contains_key(*path))
76            .collect()
77    }
78}
79
80/// Generator that outputs code into multiple files.
81///
82/// This generator follows Rust's module conventions:
83///
84/// **Modern style** (`use_mod_rs = false`):
85/// ```text
86/// src/
87///   lib.rs           # crate root
88///   models.rs        # mod models
89///   models/
90///     user.rs        # mod models::user
91///     post.rs        # mod models::post
92/// ```
93///
94/// **Legacy style** (`use_mod_rs = true`):
95/// ```text
96/// src/
97///   lib.rs           # crate root
98///   models/
99///     mod.rs         # mod models
100///     user.rs        # mod models::user
101///     post.rs        # mod models::post
102/// ```
103#[derive(Debug, Clone)]
104pub struct MultiFileGenerator {
105    /// Use mod.rs style (legacy) instead of module.rs style (modern).
106    pub use_mod_rs: bool,
107    /// Root file name (default: "lib.rs").
108    pub root_file: String,
109}
110
111impl Default for MultiFileGenerator {
112    fn default() -> Self {
113        Self {
114            use_mod_rs: false,
115            root_file: "lib.rs".to_string(),
116        }
117    }
118}
119
120impl MultiFileGenerator {
121    /// Create a new multi-file generator with modern style.
122    pub fn new() -> Self {
123        Self::default()
124    }
125
126    /// Use legacy mod.rs style.
127    pub fn with_mod_rs_style(mut self) -> Self {
128        self.use_mod_rs = true;
129        self
130    }
131
132    /// Set root file name (e.g., "main.rs" for binaries).
133    pub fn with_root_file(mut self, name: impl Into<String>) -> Self {
134        self.root_file = name.into();
135        self
136    }
137
138    /// Generate files from a ModuleTree.
139    pub fn generate(&self, tree: &ModuleTree) -> Result<GeneratedFiles, crate::pure::ToSynError> {
140        let mut files = GeneratedFiles::new();
141        let root_path = PathBuf::from(&self.root_file);
142
143        self.generate_module(tree, &root_path, &PathBuf::new(), true, &mut files)?;
144
145        Ok(files)
146    }
147
148    /// Generate a module and its children recursively.
149    ///
150    /// - `tree`: The module tree to generate
151    /// - `file_path`: Path to the file for this module
152    /// - `dir_path`: Directory path for child modules
153    /// - `is_root`: Whether this is the crate root
154    /// - `files`: Output map
155    fn generate_module(
156        &self,
157        tree: &ModuleTree,
158        file_path: &Path,
159        dir_path: &Path,
160        is_root: bool,
161        files: &mut GeneratedFiles,
162    ) -> Result<(), crate::pure::ToSynError> {
163        // Build items for this module
164        let mut items = Vec::new();
165
166        // Add uses first
167        for u in &tree.uses {
168            items.push(PureItem::Use(u.clone()));
169        }
170
171        // Add items
172        items.extend(tree.items.iter().cloned());
173
174        // Add mod declarations for children (without content - they're in separate files)
175        for child in &tree.children {
176            items.push(PureItem::Mod(PureMod {
177                attrs: vec![],
178                vis: child.vis.clone(),
179                name: child.name.clone(),
180                items: vec![], // External module - no inline content
181            }));
182        }
183
184        // Create PureFile for this module
185        let pure_file = PureFile {
186            attrs: tree.inner_attrs.clone(),
187            items,
188        };
189
190        let source = pure_file.to_source()?;
191        files.files.insert(
192            file_path.to_path_buf(),
193            GeneratedSource { source, pure_file },
194        );
195
196        // Generate child modules recursively
197        for child in &tree.children {
198            let (child_file_path, child_dir_path) = if self.use_mod_rs {
199                // Legacy style: module/mod.rs
200                let child_dir = dir_path.join(&child.name);
201                let child_file = child_dir.join("mod.rs");
202                (child_file, child_dir)
203            } else {
204                // Modern style: module.rs + module/
205                if child.children.is_empty() && is_root {
206                    // Leaf module at root level: just module.rs
207                    let child_file = dir_path.join(format!("{}.rs", child.name));
208                    (child_file, dir_path.join(&child.name))
209                } else if child.children.is_empty() {
210                    // Leaf module in subdir: dir/module.rs
211                    let child_file = dir_path.join(format!("{}.rs", child.name));
212                    (child_file, dir_path.join(&child.name))
213                } else {
214                    // Non-leaf module: module.rs + module/
215                    let child_file = dir_path.join(format!("{}.rs", child.name));
216                    let child_dir = dir_path.join(&child.name);
217                    (child_file, child_dir)
218                }
219            };
220
221            self.generate_module(child, &child_file_path, &child_dir_path, false, files)?;
222        }
223        Ok(())
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::*;
230    use crate::pure::{
231        PureBlock, PureFields, PureFn, PureGenerics, PureStruct, PureUse, PureUseTree, PureVis,
232    };
233
234    fn make_struct(name: &str) -> PureItem {
235        PureItem::Struct(PureStruct {
236            attrs: vec![],
237            vis: PureVis::Public,
238            name: name.to_string(),
239            generics: PureGenerics::default(),
240            fields: PureFields::Unit,
241        })
242    }
243
244    fn make_fn(name: &str) -> PureItem {
245        PureItem::Fn(PureFn {
246            attrs: vec![],
247            vis: PureVis::Public,
248            is_async: false,
249            is_async_inferred: false,
250            is_const: false,
251            is_unsafe: false,
252            abi: None,
253            name: name.to_string(),
254            generics: PureGenerics::default(),
255            params: vec![],
256            ret: None,
257            body: PureBlock::default(),
258        })
259    }
260
261    fn make_use(path: &str) -> PureUse {
262        let parts: Vec<&str> = path.split("::").collect();
263        let tree = build_use_tree(&parts);
264        PureUse {
265            vis: PureVis::Private,
266            tree,
267        }
268    }
269
270    fn build_use_tree(parts: &[&str]) -> PureUseTree {
271        if parts.len() == 1 {
272            PureUseTree::Name(parts[0].to_string())
273        } else {
274            PureUseTree::Path {
275                path: parts[0].to_string(),
276                tree: Box::new(build_use_tree(&parts[1..])),
277            }
278        }
279    }
280
281    // ========================================================================
282    // Basic Tests
283    // ========================================================================
284
285    #[test]
286    fn test_multi_file_single_module() {
287        let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
288
289        let generator = MultiFileGenerator::new();
290        let result = generator.generate(&tree).unwrap();
291
292        assert_eq!(result.len(), 1);
293        assert!(result.get(Path::new("lib.rs")).is_some());
294
295        let lib = result.get(Path::new("lib.rs")).unwrap();
296        assert!(lib.source.contains("struct Config"));
297    }
298
299    #[test]
300    fn test_multi_file_with_child_modern_style() {
301        let tree = ModuleTree::crate_root()
302            .with_item(make_struct("Config"))
303            .with_child(
304                ModuleTree::new("models")
305                    .with_vis(PureVis::Public)
306                    .with_item(make_struct("User")),
307            );
308
309        let generator = MultiFileGenerator::new();
310        let result = generator.generate(&tree).unwrap();
311
312        assert_eq!(result.len(), 2);
313
314        // lib.rs should have mod declaration
315        let lib = result.get(Path::new("lib.rs")).unwrap();
316        assert!(lib.source.contains("struct Config"));
317        assert!(lib.source.contains("pub mod models;"));
318        assert!(!lib.source.contains("struct User")); // User is in separate file
319
320        // models.rs should have User
321        let models = result.get(Path::new("models.rs")).unwrap();
322        assert!(models.source.contains("struct User"));
323    }
324
325    #[test]
326    fn test_multi_file_with_child_mod_rs_style() {
327        let tree = ModuleTree::crate_root()
328            .with_item(make_struct("Config"))
329            .with_child(
330                ModuleTree::new("models")
331                    .with_vis(PureVis::Public)
332                    .with_item(make_struct("User")),
333            );
334
335        let generator = MultiFileGenerator::new().with_mod_rs_style();
336        let result = generator.generate(&tree).unwrap();
337
338        assert_eq!(result.len(), 2);
339
340        // lib.rs should have mod declaration
341        let lib = result.get(Path::new("lib.rs")).unwrap();
342        assert!(lib.source.contains("pub mod models;"));
343
344        // models/mod.rs should have User
345        let models = result.get(Path::new("models/mod.rs")).unwrap();
346        assert!(models.source.contains("struct User"));
347    }
348
349    #[test]
350    fn test_multi_file_nested_modules() {
351        let tree = ModuleTree::crate_root().with_child(
352            ModuleTree::new("models")
353                .with_vis(PureVis::Public)
354                .with_item(make_struct("User"))
355                .with_child(
356                    ModuleTree::new("dto")
357                        .with_vis(PureVis::Public)
358                        .with_item(make_struct("UserDto")),
359                ),
360        );
361
362        let generator = MultiFileGenerator::new();
363        let result = generator.generate(&tree).unwrap();
364
365        assert_eq!(result.len(), 3);
366
367        // Check file structure
368        assert!(result.get(Path::new("lib.rs")).is_some());
369        assert!(result.get(Path::new("models.rs")).is_some());
370        assert!(result.get(Path::new("models/dto.rs")).is_some());
371
372        // Check content
373        let lib = result.get(Path::new("lib.rs")).unwrap();
374        assert!(lib.source.contains("pub mod models;"));
375
376        let models = result.get(Path::new("models.rs")).unwrap();
377        assert!(models.source.contains("struct User"));
378        assert!(models.source.contains("pub mod dto;"));
379
380        let dto = result.get(Path::new("models/dto.rs")).unwrap();
381        assert!(dto.source.contains("struct UserDto"));
382    }
383
384    #[test]
385    fn test_multi_file_with_uses() {
386        let tree = ModuleTree::crate_root()
387            .with_use(make_use("std::io"))
388            .with_child(
389                ModuleTree::new("utils")
390                    .with_use(make_use("std::fmt"))
391                    .with_item(make_fn("helper")),
392            );
393
394        let generator = MultiFileGenerator::new();
395        let result = generator.generate(&tree).unwrap();
396
397        let lib = result.get(Path::new("lib.rs")).unwrap();
398        assert!(lib.source.contains("use std") && lib.source.contains("io"));
399
400        let utils = result.get(Path::new("utils.rs")).unwrap();
401        assert!(utils.source.contains("use std") && utils.source.contains("fmt"));
402    }
403
404    // ========================================================================
405    // Diff Tests
406    // ========================================================================
407
408    #[test]
409    fn test_diff_new_file() {
410        let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
411
412        let generator = MultiFileGenerator::new();
413        let generated = generator.generate(&tree).unwrap();
414
415        // No existing files
416        let existing: HashMap<PathBuf, PureFile> = HashMap::new();
417        let diff = generated.diff(&existing);
418
419        // All files should be in diff (they're new)
420        assert_eq!(diff.len(), 1);
421    }
422
423    #[test]
424    fn test_diff_unchanged() {
425        let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
426
427        let generator = MultiFileGenerator::new();
428        let generated = generator.generate(&tree).unwrap();
429
430        // Same PureFile as generated
431        let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
432        existing.insert(
433            PathBuf::from("lib.rs"),
434            generated
435                .get(Path::new("lib.rs"))
436                .unwrap()
437                .pure_file
438                .clone(),
439        );
440
441        let diff = generated.diff(&existing);
442
443        // No changes
444        assert_eq!(diff.len(), 0);
445    }
446
447    #[test]
448    fn test_diff_changed() {
449        let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
450
451        let generator = MultiFileGenerator::new();
452        let generated = generator.generate(&tree).unwrap();
453
454        // Different PureFile
455        let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
456        existing.insert(
457            PathBuf::from("lib.rs"),
458            PureFile {
459                attrs: vec![],
460                items: vec![make_struct("OldConfig")], // Different!
461            },
462        );
463
464        let diff = generated.diff(&existing);
465
466        // lib.rs changed
467        assert_eq!(diff.len(), 1);
468        assert!(diff.get(Path::new("lib.rs")).is_some());
469    }
470
471    #[test]
472    fn test_deleted_paths() {
473        let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
474
475        let generator = MultiFileGenerator::new();
476        let generated = generator.generate(&tree).unwrap();
477
478        // Existing has extra file
479        let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
480        existing.insert(PathBuf::from("lib.rs"), PureFile::default());
481        existing.insert(PathBuf::from("old_module.rs"), PureFile::default());
482
483        let deleted = generated.deleted_paths(&existing);
484
485        assert_eq!(deleted.len(), 1);
486        assert_eq!(deleted[0], &PathBuf::from("old_module.rs"));
487    }
488
489    // ========================================================================
490    // Main.rs Tests
491    // ========================================================================
492
493    #[test]
494    fn test_multi_file_main_rs() {
495        let tree = ModuleTree::crate_root().with_item(make_fn("main"));
496
497        let generator = MultiFileGenerator::new().with_root_file("main.rs");
498        let result = generator.generate(&tree).unwrap();
499
500        assert!(result.get(Path::new("main.rs")).is_some());
501        assert!(result.get(Path::new("lib.rs")).is_none());
502    }
503
504    // ========================================================================
505    // Deep Nesting Tests
506    // ========================================================================
507
508    #[test]
509    fn test_multi_file_deep_nesting() {
510        let tree = ModuleTree::crate_root().with_child(ModuleTree::new("a").with_child(
511            ModuleTree::new("b").with_child(ModuleTree::new("c").with_item(make_struct("Deep"))),
512        ));
513
514        let generator = MultiFileGenerator::new();
515        let result = generator.generate(&tree).unwrap();
516
517        // Should have: lib.rs, a.rs, a/b.rs, a/b/c.rs
518        assert_eq!(result.len(), 4);
519        assert!(result.get(Path::new("lib.rs")).is_some());
520        assert!(result.get(Path::new("a.rs")).is_some());
521        assert!(result.get(Path::new("a/b.rs")).is_some());
522        assert!(result.get(Path::new("a/b/c.rs")).is_some());
523
524        let c = result.get(Path::new("a/b/c.rs")).unwrap();
525        assert!(c.source.contains("struct Deep"));
526    }
527
528    #[test]
529    fn test_multi_file_deep_nesting_mod_rs() {
530        let tree = ModuleTree::crate_root().with_child(
531            ModuleTree::new("a").with_child(ModuleTree::new("b").with_item(make_struct("Deep"))),
532        );
533
534        let generator = MultiFileGenerator::new().with_mod_rs_style();
535        let result = generator.generate(&tree).unwrap();
536
537        // Should have: lib.rs, a/mod.rs, a/b/mod.rs
538        assert_eq!(result.len(), 3);
539        assert!(result.get(Path::new("lib.rs")).is_some());
540        assert!(result.get(Path::new("a/mod.rs")).is_some());
541        assert!(result.get(Path::new("a/b/mod.rs")).is_some());
542    }
543
544    // ========================================================================
545    // Valid Rust Tests
546    // ========================================================================
547
548    #[test]
549    fn test_all_generated_files_are_valid_rust() {
550        let tree = ModuleTree::crate_root()
551            .with_use(make_use("std::collections::HashMap"))
552            .with_item(make_struct("App"))
553            .with_child(
554                ModuleTree::new("models")
555                    .with_vis(PureVis::Public)
556                    .with_item(make_struct("User"))
557                    .with_child(
558                        ModuleTree::new("dto")
559                            .with_vis(PureVis::Public)
560                            .with_item(make_struct("UserDto")),
561                    ),
562            )
563            .with_child(ModuleTree::new("utils").with_item(make_fn("helper")));
564
565        let generator = MultiFileGenerator::new();
566        let result = generator.generate(&tree).unwrap();
567
568        for (path, generated) in &result.files {
569            syn::parse_str::<syn::File>(&generated.source).unwrap_or_else(|_| {
570                panic!(
571                    "File {} should be valid Rust:\n{}",
572                    path.display(),
573                    generated.source
574                )
575            });
576        }
577    }
578}