use super::{GeneratedSource, ModuleTree};
use crate::pure::{PureFile, PureItem, PureMod};
use std::collections::HashMap;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Default)]
pub struct GeneratedFiles {
pub files: HashMap<PathBuf, GeneratedSource>,
}
impl GeneratedFiles {
pub fn new() -> Self {
Self::default()
}
pub fn paths(&self) -> impl Iterator<Item = &PathBuf> {
self.files.keys()
}
pub fn get(&self, path: &Path) -> Option<&GeneratedSource> {
self.files.get(path)
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn diff(&self, existing: &HashMap<PathBuf, PureFile>) -> GeneratedFiles {
let mut changed = GeneratedFiles::new();
for (path, generated) in &self.files {
let is_changed = match existing.get(path) {
None => true, Some(existing_pure) => {
format!("{:?}", generated.pure_file) != format!("{:?}", existing_pure)
}
};
if is_changed {
changed.files.insert(path.clone(), generated.clone());
}
}
changed
}
pub fn deleted_paths<'a>(&self, existing: &'a HashMap<PathBuf, PureFile>) -> Vec<&'a PathBuf> {
existing
.keys()
.filter(|path| !self.files.contains_key(*path))
.collect()
}
}
#[derive(Debug, Clone)]
pub struct MultiFileGenerator {
pub use_mod_rs: bool,
pub root_file: String,
}
impl Default for MultiFileGenerator {
fn default() -> Self {
Self {
use_mod_rs: false,
root_file: "lib.rs".to_string(),
}
}
}
impl MultiFileGenerator {
pub fn new() -> Self {
Self::default()
}
pub fn with_mod_rs_style(mut self) -> Self {
self.use_mod_rs = true;
self
}
pub fn with_root_file(mut self, name: impl Into<String>) -> Self {
self.root_file = name.into();
self
}
pub fn generate(&self, tree: &ModuleTree) -> Result<GeneratedFiles, crate::pure::ToSynError> {
let mut files = GeneratedFiles::new();
let root_path = PathBuf::from(&self.root_file);
self.generate_module(tree, &root_path, &PathBuf::new(), true, &mut files)?;
Ok(files)
}
fn generate_module(
&self,
tree: &ModuleTree,
file_path: &Path,
dir_path: &Path,
is_root: bool,
files: &mut GeneratedFiles,
) -> Result<(), crate::pure::ToSynError> {
let mut items = Vec::new();
for u in &tree.uses {
items.push(PureItem::Use(u.clone()));
}
items.extend(tree.items.iter().cloned());
for child in &tree.children {
items.push(PureItem::Mod(PureMod {
attrs: vec![],
vis: child.vis.clone(),
name: child.name.clone(),
items: vec![], }));
}
let pure_file = PureFile {
attrs: tree.inner_attrs.clone(),
items,
};
let source = pure_file.to_source()?;
files.files.insert(
file_path.to_path_buf(),
GeneratedSource { source, pure_file },
);
for child in &tree.children {
let (child_file_path, child_dir_path) = if self.use_mod_rs {
let child_dir = dir_path.join(&child.name);
let child_file = child_dir.join("mod.rs");
(child_file, child_dir)
} else {
if child.children.is_empty() && is_root {
let child_file = dir_path.join(format!("{}.rs", child.name));
(child_file, dir_path.join(&child.name))
} else if child.children.is_empty() {
let child_file = dir_path.join(format!("{}.rs", child.name));
(child_file, dir_path.join(&child.name))
} else {
let child_file = dir_path.join(format!("{}.rs", child.name));
let child_dir = dir_path.join(&child.name);
(child_file, child_dir)
}
};
self.generate_module(child, &child_file_path, &child_dir_path, false, files)?;
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pure::{
PureBlock, PureFields, PureFn, PureGenerics, PureStruct, PureUse, PureUseTree, PureVis,
};
fn make_struct(name: &str) -> PureItem {
PureItem::Struct(PureStruct {
attrs: vec![],
vis: PureVis::Public,
name: name.to_string(),
generics: PureGenerics::default(),
fields: PureFields::Unit,
})
}
fn make_fn(name: &str) -> PureItem {
PureItem::Fn(PureFn {
attrs: vec![],
vis: PureVis::Public,
is_async: false,
is_async_inferred: false,
is_const: false,
is_unsafe: false,
abi: None,
name: name.to_string(),
generics: PureGenerics::default(),
params: vec![],
ret: None,
body: PureBlock::default(),
})
}
fn make_use(path: &str) -> PureUse {
let parts: Vec<&str> = path.split("::").collect();
let tree = build_use_tree(&parts);
PureUse {
vis: PureVis::Private,
tree,
}
}
fn build_use_tree(parts: &[&str]) -> PureUseTree {
if parts.len() == 1 {
PureUseTree::Name(parts[0].to_string())
} else {
PureUseTree::Path {
path: parts[0].to_string(),
tree: Box::new(build_use_tree(&parts[1..])),
}
}
}
#[test]
fn test_multi_file_single_module() {
let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
let generator = MultiFileGenerator::new();
let result = generator.generate(&tree).unwrap();
assert_eq!(result.len(), 1);
assert!(result.get(Path::new("lib.rs")).is_some());
let lib = result.get(Path::new("lib.rs")).unwrap();
assert!(lib.source.contains("struct Config"));
}
#[test]
fn test_multi_file_with_child_modern_style() {
let tree = ModuleTree::crate_root()
.with_item(make_struct("Config"))
.with_child(
ModuleTree::new("models")
.with_vis(PureVis::Public)
.with_item(make_struct("User")),
);
let generator = MultiFileGenerator::new();
let result = generator.generate(&tree).unwrap();
assert_eq!(result.len(), 2);
let lib = result.get(Path::new("lib.rs")).unwrap();
assert!(lib.source.contains("struct Config"));
assert!(lib.source.contains("pub mod models;"));
assert!(!lib.source.contains("struct User"));
let models = result.get(Path::new("models.rs")).unwrap();
assert!(models.source.contains("struct User"));
}
#[test]
fn test_multi_file_with_child_mod_rs_style() {
let tree = ModuleTree::crate_root()
.with_item(make_struct("Config"))
.with_child(
ModuleTree::new("models")
.with_vis(PureVis::Public)
.with_item(make_struct("User")),
);
let generator = MultiFileGenerator::new().with_mod_rs_style();
let result = generator.generate(&tree).unwrap();
assert_eq!(result.len(), 2);
let lib = result.get(Path::new("lib.rs")).unwrap();
assert!(lib.source.contains("pub mod models;"));
let models = result.get(Path::new("models/mod.rs")).unwrap();
assert!(models.source.contains("struct User"));
}
#[test]
fn test_multi_file_nested_modules() {
let tree = ModuleTree::crate_root().with_child(
ModuleTree::new("models")
.with_vis(PureVis::Public)
.with_item(make_struct("User"))
.with_child(
ModuleTree::new("dto")
.with_vis(PureVis::Public)
.with_item(make_struct("UserDto")),
),
);
let generator = MultiFileGenerator::new();
let result = generator.generate(&tree).unwrap();
assert_eq!(result.len(), 3);
assert!(result.get(Path::new("lib.rs")).is_some());
assert!(result.get(Path::new("models.rs")).is_some());
assert!(result.get(Path::new("models/dto.rs")).is_some());
let lib = result.get(Path::new("lib.rs")).unwrap();
assert!(lib.source.contains("pub mod models;"));
let models = result.get(Path::new("models.rs")).unwrap();
assert!(models.source.contains("struct User"));
assert!(models.source.contains("pub mod dto;"));
let dto = result.get(Path::new("models/dto.rs")).unwrap();
assert!(dto.source.contains("struct UserDto"));
}
#[test]
fn test_multi_file_with_uses() {
let tree = ModuleTree::crate_root()
.with_use(make_use("std::io"))
.with_child(
ModuleTree::new("utils")
.with_use(make_use("std::fmt"))
.with_item(make_fn("helper")),
);
let generator = MultiFileGenerator::new();
let result = generator.generate(&tree).unwrap();
let lib = result.get(Path::new("lib.rs")).unwrap();
assert!(lib.source.contains("use std") && lib.source.contains("io"));
let utils = result.get(Path::new("utils.rs")).unwrap();
assert!(utils.source.contains("use std") && utils.source.contains("fmt"));
}
#[test]
fn test_diff_new_file() {
let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
let generator = MultiFileGenerator::new();
let generated = generator.generate(&tree).unwrap();
let existing: HashMap<PathBuf, PureFile> = HashMap::new();
let diff = generated.diff(&existing);
assert_eq!(diff.len(), 1);
}
#[test]
fn test_diff_unchanged() {
let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
let generator = MultiFileGenerator::new();
let generated = generator.generate(&tree).unwrap();
let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
existing.insert(
PathBuf::from("lib.rs"),
generated
.get(Path::new("lib.rs"))
.unwrap()
.pure_file
.clone(),
);
let diff = generated.diff(&existing);
assert_eq!(diff.len(), 0);
}
#[test]
fn test_diff_changed() {
let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
let generator = MultiFileGenerator::new();
let generated = generator.generate(&tree).unwrap();
let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
existing.insert(
PathBuf::from("lib.rs"),
PureFile {
attrs: vec![],
items: vec![make_struct("OldConfig")], },
);
let diff = generated.diff(&existing);
assert_eq!(diff.len(), 1);
assert!(diff.get(Path::new("lib.rs")).is_some());
}
#[test]
fn test_deleted_paths() {
let tree = ModuleTree::crate_root().with_item(make_struct("Config"));
let generator = MultiFileGenerator::new();
let generated = generator.generate(&tree).unwrap();
let mut existing: HashMap<PathBuf, PureFile> = HashMap::new();
existing.insert(PathBuf::from("lib.rs"), PureFile::default());
existing.insert(PathBuf::from("old_module.rs"), PureFile::default());
let deleted = generated.deleted_paths(&existing);
assert_eq!(deleted.len(), 1);
assert_eq!(deleted[0], &PathBuf::from("old_module.rs"));
}
#[test]
fn test_multi_file_main_rs() {
let tree = ModuleTree::crate_root().with_item(make_fn("main"));
let generator = MultiFileGenerator::new().with_root_file("main.rs");
let result = generator.generate(&tree).unwrap();
assert!(result.get(Path::new("main.rs")).is_some());
assert!(result.get(Path::new("lib.rs")).is_none());
}
#[test]
fn test_multi_file_deep_nesting() {
let tree = ModuleTree::crate_root().with_child(ModuleTree::new("a").with_child(
ModuleTree::new("b").with_child(ModuleTree::new("c").with_item(make_struct("Deep"))),
));
let generator = MultiFileGenerator::new();
let result = generator.generate(&tree).unwrap();
assert_eq!(result.len(), 4);
assert!(result.get(Path::new("lib.rs")).is_some());
assert!(result.get(Path::new("a.rs")).is_some());
assert!(result.get(Path::new("a/b.rs")).is_some());
assert!(result.get(Path::new("a/b/c.rs")).is_some());
let c = result.get(Path::new("a/b/c.rs")).unwrap();
assert!(c.source.contains("struct Deep"));
}
#[test]
fn test_multi_file_deep_nesting_mod_rs() {
let tree = ModuleTree::crate_root().with_child(
ModuleTree::new("a").with_child(ModuleTree::new("b").with_item(make_struct("Deep"))),
);
let generator = MultiFileGenerator::new().with_mod_rs_style();
let result = generator.generate(&tree).unwrap();
assert_eq!(result.len(), 3);
assert!(result.get(Path::new("lib.rs")).is_some());
assert!(result.get(Path::new("a/mod.rs")).is_some());
assert!(result.get(Path::new("a/b/mod.rs")).is_some());
}
#[test]
fn test_all_generated_files_are_valid_rust() {
let tree = ModuleTree::crate_root()
.with_use(make_use("std::collections::HashMap"))
.with_item(make_struct("App"))
.with_child(
ModuleTree::new("models")
.with_vis(PureVis::Public)
.with_item(make_struct("User"))
.with_child(
ModuleTree::new("dto")
.with_vis(PureVis::Public)
.with_item(make_struct("UserDto")),
),
)
.with_child(ModuleTree::new("utils").with_item(make_fn("helper")));
let generator = MultiFileGenerator::new();
let result = generator.generate(&tree).unwrap();
for (path, generated) in &result.files {
syn::parse_str::<syn::File>(&generated.source).unwrap_or_else(|_| {
panic!(
"File {} should be valid Rust:\n{}",
path.display(),
generated.source
)
});
}
}
}