1use super::{GeneratedSource, ModuleTree};
7use crate::pure::{PureFile, PureItem, PureMod};
8use std::collections::HashMap;
9use std::path::{Path, PathBuf};
10
11#[derive(Debug, Clone, Default)]
13pub struct GeneratedFiles {
14 pub files: HashMap<PathBuf, GeneratedSource>,
16}
17
18impl GeneratedFiles {
19 pub fn new() -> Self {
21 Self::default()
22 }
23
24 pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
26 self.files.keys()
27 }
28
29 pub fn get(&self, path: &Path) -> Option<&GeneratedSource> {
31 self.files.get(path)
32 }
33
34 pub fn len(&self) -> usize {
36 self.files.len()
37 }
38
39 pub fn is_empty(&self) -> bool {
41 self.files.is_empty()
42 }
43
44 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, Some(existing_pure) => {
56 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 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#[derive(Debug, Clone)]
104pub struct MultiFileGenerator {
105 pub use_mod_rs: bool,
107 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 pub fn new() -> Self {
123 Self::default()
124 }
125
126 pub fn with_mod_rs_style(mut self) -> Self {
128 self.use_mod_rs = true;
129 self
130 }
131
132 pub fn with_root_file(mut self, name: impl Into<String>) -> Self {
134 self.root_file = name.into();
135 self
136 }
137
138 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 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 let mut items = Vec::new();
165
166 for u in &tree.uses {
168 items.push(PureItem::Use(u.clone()));
169 }
170
171 items.extend(tree.items.iter().cloned());
173
174 for child in &tree.children {
179 items.push(PureItem::Mod(PureMod {
180 attrs: child.outer_attrs.clone(),
181 vis: child.vis.clone(),
182 name: child.name.clone(),
183 items: vec![], scope: Default::default(),
185 }));
186 }
187
188 let pure_file = PureFile {
190 attrs: tree.inner_attrs.clone(),
191 items,
192 };
193
194 let source = pure_file.to_source()?;
195 files.files.insert(
196 file_path.to_path_buf(),
197 GeneratedSource { source, pure_file },
198 );
199
200 for child in &tree.children {
202 let (child_file_path, child_dir_path) = if self.use_mod_rs {
203 let child_dir = dir_path.join(&child.name);
205 let child_file = child_dir.join("mod.rs");
206 (child_file, child_dir)
207 } else {
208 if child.children.is_empty() && is_root {
210 let child_file = dir_path.join(format!("{}.rs", child.name));
212 (child_file, dir_path.join(&child.name))
213 } else if child.children.is_empty() {
214 let child_file = dir_path.join(format!("{}.rs", child.name));
216 (child_file, dir_path.join(&child.name))
217 } else {
218 let child_file = dir_path.join(format!("{}.rs", child.name));
220 let child_dir = dir_path.join(&child.name);
221 (child_file, child_dir)
222 }
223 };
224
225 self.generate_module(child, &child_file_path, &child_dir_path, false, files)?;
226 }
227 Ok(())
228 }
229}
230
231#[cfg(test)]
232mod tests {
233 use super::*;
234 use crate::pure::{
235 PureAttrMeta, PureAttribute, PureBlock, PureFields, PureFn, PureGenerics, PureStruct,
236 PureUse, PureUseTree, PureVis,
237 };
238
239 fn make_struct(name: &str) -> PureItem {
240 PureItem::Struct(PureStruct {
241 attrs: vec![],
242 vis: PureVis::Public,
243 name: name.to_string(),
244 generics: PureGenerics::default(),
245 fields: PureFields::Unit,
246 })
247 }
248
249 fn make_fn(name: &str) -> PureItem {
250 PureItem::Fn(PureFn {
251 attrs: vec![],
252 vis: PureVis::Public,
253 is_async: false,
254 is_async_inferred: false,
255 is_const: false,
256 is_unsafe: false,
257 abi: None,
258 name: name.to_string(),
259 generics: PureGenerics::default(),
260 params: vec![],
261 ret: None,
262 body: PureBlock::default(),
263 })
264 }
265
266 fn make_use(path: &str) -> PureUse {
267 let parts: Vec<&str> = path.split("::").collect();
268 let tree = build_use_tree(&parts);
269 PureUse {
270 vis: PureVis::Private,
271 tree,
272 }
273 }
274
275 fn build_use_tree(parts: &[&str]) -> PureUseTree {
276 if parts.len() == 1 {
277 PureUseTree::Name(parts[0].to_string())
278 } else {
279 PureUseTree::Path {
280 path: parts[0].to_string(),
281 tree: Box::new(build_use_tree(&parts[1..])),
282 }
283 }
284 }
285
286 #[test]
291 fn test_multi_file_single_module() {
292 let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
293
294 let generator = MultiFileGenerator::new();
295 let result = generator.generate(&tree).unwrap();
296
297 assert_eq!(result.len(), 1);
298 assert!(result.get(Path::new("lib.rs")).is_some());
299
300 let lib = result.get(Path::new("lib.rs")).unwrap();
301 assert!(lib.source.contains("struct Config"));
302 }
303
304 #[test]
305 fn test_multi_file_with_child_modern_style() {
306 let tree = ModuleTree::crate_root()
307 .with_item(make_struct("Config"))
308 .with_child(
309 ModuleTree::new("models")
310 .with_vis(PureVis::Public)
311 .with_item(make_struct("User")),
312 );
313
314 let generator = MultiFileGenerator::new();
315 let result = generator.generate(&tree).unwrap();
316
317 assert_eq!(result.len(), 2);
318
319 let lib = result.get(Path::new("lib.rs")).unwrap();
321 assert!(lib.source.contains("struct Config"));
322 assert!(lib.source.contains("pub mod models;"));
323 assert!(!lib.source.contains("struct User")); let models = result.get(Path::new("models.rs")).unwrap();
327 assert!(models.source.contains("struct User"));
328 }
329
330 #[test]
331 fn test_multi_file_with_child_mod_rs_style() {
332 let tree = ModuleTree::crate_root()
333 .with_item(make_struct("Config"))
334 .with_child(
335 ModuleTree::new("models")
336 .with_vis(PureVis::Public)
337 .with_item(make_struct("User")),
338 );
339
340 let generator = MultiFileGenerator::new().with_mod_rs_style();
341 let result = generator.generate(&tree).unwrap();
342
343 assert_eq!(result.len(), 2);
344
345 let lib = result.get(Path::new("lib.rs")).unwrap();
347 assert!(lib.source.contains("pub mod models;"));
348
349 let models = result.get(Path::new("models/mod.rs")).unwrap();
351 assert!(models.source.contains("struct User"));
352 }
353
354 #[test]
355 fn test_multi_file_nested_modules() {
356 let tree = ModuleTree::crate_root().with_child(
357 ModuleTree::new("models")
358 .with_vis(PureVis::Public)
359 .with_item(make_struct("User"))
360 .with_child(
361 ModuleTree::new("dto")
362 .with_vis(PureVis::Public)
363 .with_item(make_struct("UserDto")),
364 ),
365 );
366
367 let generator = MultiFileGenerator::new();
368 let result = generator.generate(&tree).unwrap();
369
370 assert_eq!(result.len(), 3);
371
372 assert!(result.get(Path::new("lib.rs")).is_some());
374 assert!(result.get(Path::new("models.rs")).is_some());
375 assert!(result.get(Path::new("models/dto.rs")).is_some());
376
377 let lib = result.get(Path::new("lib.rs")).unwrap();
379 assert!(lib.source.contains("pub mod models;"));
380
381 let models = result.get(Path::new("models.rs")).unwrap();
382 assert!(models.source.contains("struct User"));
383 assert!(models.source.contains("pub mod dto;"));
384
385 let dto = result.get(Path::new("models/dto.rs")).unwrap();
386 assert!(dto.source.contains("struct UserDto"));
387 }
388
389 #[test]
390 fn test_multi_file_with_uses() {
391 let tree = ModuleTree::crate_root()
392 .with_use(make_use("std::io"))
393 .with_child(
394 ModuleTree::new("utils")
395 .with_use(make_use("std::fmt"))
396 .with_item(make_fn("helper")),
397 );
398
399 let generator = MultiFileGenerator::new();
400 let result = generator.generate(&tree).unwrap();
401
402 let lib = result.get(Path::new("lib.rs")).unwrap();
403 assert!(lib.source.contains("use std") && lib.source.contains("io"));
404
405 let utils = result.get(Path::new("utils.rs")).unwrap();
406 assert!(utils.source.contains("use std") && utils.source.contains("fmt"));
407 }
408
409 #[test]
414 fn test_diff_new_file() {
415 let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
416
417 let generator = MultiFileGenerator::new();
418 let generated = generator.generate(&tree).unwrap();
419
420 let existing: HashMap<PathBuf, PureFile> = HashMap::new();
422 let diff = generated.diff(&existing);
423
424 assert_eq!(diff.len(), 1);
426 }
427
428 #[test]
429 fn test_diff_unchanged() {
430 let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
431
432 let generator = MultiFileGenerator::new();
433 let generated = generator.generate(&tree).unwrap();
434
435 let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
437 existing.insert(
438 PathBuf::from("lib.rs"),
439 generated
440 .get(Path::new("lib.rs"))
441 .unwrap()
442 .pure_file
443 .clone(),
444 );
445
446 let diff = generated.diff(&existing);
447
448 assert_eq!(diff.len(), 0);
450 }
451
452 #[test]
453 fn test_diff_changed() {
454 let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
455
456 let generator = MultiFileGenerator::new();
457 let generated = generator.generate(&tree).unwrap();
458
459 let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
461 existing.insert(
462 PathBuf::from("lib.rs"),
463 PureFile {
464 attrs: vec![],
465 items: vec![make_struct("OldConfig")], },
467 );
468
469 let diff = generated.diff(&existing);
470
471 assert_eq!(diff.len(), 1);
473 assert!(diff.get(Path::new("lib.rs")).is_some());
474 }
475
476 #[test]
477 fn test_deleted_paths() {
478 let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
479
480 let generator = MultiFileGenerator::new();
481 let generated = generator.generate(&tree).unwrap();
482
483 let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
485 existing.insert(PathBuf::from("lib.rs"), PureFile::default());
486 existing.insert(PathBuf::from("old_module.rs"), PureFile::default());
487
488 let deleted = generated.deleted_paths(&existing);
489
490 assert_eq!(deleted.len(), 1);
491 assert_eq!(deleted[0], &PathBuf::from("old_module.rs"));
492 }
493
494 #[test]
499 fn test_multi_file_main_rs() {
500 let tree = ModuleTree::crate_root().with_item(make_fn("main"));
501
502 let generator = MultiFileGenerator::new().with_root_file("main.rs");
503 let result = generator.generate(&tree).unwrap();
504
505 assert!(result.get(Path::new("main.rs")).is_some());
506 assert!(result.get(Path::new("lib.rs")).is_none());
507 }
508
509 #[test]
514 fn test_multi_file_deep_nesting() {
515 let tree = ModuleTree::crate_root().with_child(ModuleTree::new("a").with_child(
516 ModuleTree::new("b").with_child(ModuleTree::new("c").with_item(make_struct("Deep"))),
517 ));
518
519 let generator = MultiFileGenerator::new();
520 let result = generator.generate(&tree).unwrap();
521
522 assert_eq!(result.len(), 4);
524 assert!(result.get(Path::new("lib.rs")).is_some());
525 assert!(result.get(Path::new("a.rs")).is_some());
526 assert!(result.get(Path::new("a/b.rs")).is_some());
527 assert!(result.get(Path::new("a/b/c.rs")).is_some());
528
529 let c = result.get(Path::new("a/b/c.rs")).unwrap();
530 assert!(c.source.contains("struct Deep"));
531 }
532
533 #[test]
534 fn test_multi_file_deep_nesting_mod_rs() {
535 let tree = ModuleTree::crate_root().with_child(
536 ModuleTree::new("a").with_child(ModuleTree::new("b").with_item(make_struct("Deep"))),
537 );
538
539 let generator = MultiFileGenerator::new().with_mod_rs_style();
540 let result = generator.generate(&tree).unwrap();
541
542 assert_eq!(result.len(), 3);
544 assert!(result.get(Path::new("lib.rs")).is_some());
545 assert!(result.get(Path::new("a/mod.rs")).is_some());
546 assert!(result.get(Path::new("a/b/mod.rs")).is_some());
547 }
548
549 #[test]
554 fn test_all_generated_files_are_valid_rust() {
555 let tree = ModuleTree::crate_root()
556 .with_use(make_use("std::collections::HashMap"))
557 .with_item(make_struct("App"))
558 .with_child(
559 ModuleTree::new("models")
560 .with_vis(PureVis::Public)
561 .with_item(make_struct("User"))
562 .with_child(
563 ModuleTree::new("dto")
564 .with_vis(PureVis::Public)
565 .with_item(make_struct("UserDto")),
566 ),
567 )
568 .with_child(ModuleTree::new("utils").with_item(make_fn("helper")));
569
570 let generator = MultiFileGenerator::new();
571 let result = generator.generate(&tree).unwrap();
572
573 for (path, generated) in &result.files {
574 syn::parse_str::<syn::File>(&generated.source).unwrap_or_else(|_| {
575 panic!(
576 "File {} should be valid Rust:\n{}",
577 path.display(),
578 generated.source
579 )
580 });
581 }
582 }
583
584 #[test]
589 fn test_multi_file_carries_outer_attrs_on_child_mod_decl() {
590 let cfg_test = PureAttribute {
597 path: "cfg".to_string(),
598 meta: PureAttrMeta::List("test".to_string()),
599 is_inner: false,
600 };
601
602 let tree = ModuleTree::crate_root()
603 .with_item(make_struct("Config"))
604 .with_child(
605 ModuleTree::new("tests")
606 .with_vis(PureVis::Private)
607 .with_outer_attr(cfg_test)
608 .with_item(make_fn("smoke")),
609 );
610
611 let generator = MultiFileGenerator::new();
612 let result = generator.generate(&tree).unwrap();
613
614 let lib = result.get(Path::new("lib.rs")).unwrap();
615 assert!(
616 lib.source.contains("#[cfg(test)]"),
617 "lib.rs must carry outer attr on `mod tests;` decl, got:\n{}",
618 lib.source
619 );
620 assert!(lib.source.contains("mod tests"));
621 }
622}