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 {
176 items.push(PureItem::Mod(PureMod {
177 attrs: vec![],
178 vis: child.vis.clone(),
179 name: child.name.clone(),
180 items: vec![], }));
182 }
183
184 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 for child in &tree.children {
198 let (child_file_path, child_dir_path) = if self.use_mod_rs {
199 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 if child.children.is_empty() && is_root {
206 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 let child_file = dir_path.join(format!("{}.rs", child.name));
212 (child_file, dir_path.join(&child.name))
213 } else {
214 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 #[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 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")); 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 let lib = result.get(Path::new("lib.rs")).unwrap();
342 assert!(lib.source.contains("pub mod models;"));
343
344 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 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 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 #[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 let existing: HashMap<PathBuf, PureFile> = HashMap::new();
417 let diff = generated.diff(&existing);
418
419 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 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 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 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")], },
462 );
463
464 let diff = generated.diff(&existing);
465
466 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 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 #[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 #[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 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 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 #[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}