use std::collections::BTreeMap;
use std::fs::{create_dir_all, write};
use std::io::Error as IoError;
use std::mem::replace;
use std::path::Path;
use std::str::FromStr;
use proc_macro2::{Ident as Ident2, TokenStream};
use quote::{format_ident, quote, ToTokens};
use super::IdentPath;
#[derive(Default, Debug)]
pub struct Module {
pub code: TokenStream,
pub usings: BTreeMap<String, bool>,
pub modules: BTreeMap<String, Module>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SubModules {
None,
Files,
Inline,
}
impl Module {
pub fn append(&mut self, code: TokenStream) -> &mut Self {
self.code.extend(code);
self
}
pub fn prepend(&mut self, code: TokenStream) -> &mut Self {
let code = replace(&mut self.code, code);
self.append(code)
}
pub fn usings<I>(&mut self, anonymous: bool, usings: I) -> &mut Self
where
I: IntoIterator,
I::Item: ToString,
{
for using in usings {
*self.usings.entry(using.to_string()).or_insert(anonymous) &= anonymous;
}
self
}
pub fn module<T>(&self, ident: T) -> Option<&Module>
where
T: AsRef<str>,
{
self.modules.get(ident.as_ref())
}
pub fn module_mut<T>(&mut self, ident: T) -> &mut Module
where
T: Into<String>,
{
self.modules.entry(ident.into()).or_default()
}
pub fn to_code(&self, tokens: &mut TokenStream, sub_modules: SubModules) {
let Self {
code,
usings,
modules,
} = self;
if sub_modules == SubModules::Files {
let modules = modules.iter().map(|(ident, _module)| {
let name = format_ident!("{ident}");
quote!(pub mod #name;)
});
tokens.extend(quote! {
#( #modules )*
});
}
let usings = render_usings(usings.iter());
tokens.extend(quote! {
#usings
#code
});
if sub_modules == SubModules::Inline {
for (ident, module) in modules {
let name = format_ident!("{ident}");
tokens.extend(quote! {
pub mod #name {
#module
}
});
}
}
}
pub fn write_to_files<P>(&self, directory: P) -> Result<(), IoError>
where
P: AsRef<Path>,
{
let directory = directory.as_ref();
self.write_to_files_with(|module: &Module, path: &Path| {
let filename = if module.modules.is_empty() {
directory.join(path).with_extension("rs")
} else {
directory.join(path).join("mod.rs")
};
create_dir_all(filename.parent().unwrap())?;
let mut code = TokenStream::new();
module.to_code(&mut code, SubModules::Files);
let code = code.to_string();
write(filename, code)?;
Ok(())
})
}
pub fn write_to_files_with<F, E>(&self, mut f: F) -> Result<(), E>
where
F: FnMut(&Module, &Path) -> Result<(), E>,
{
self.write_to_files_with_impl("", &mut f)
}
fn write_to_files_with_impl<P, F, E>(&self, path: P, f: &mut F) -> Result<(), E>
where
P: AsRef<Path>,
F: FnMut(&Module, &Path) -> Result<(), E>,
{
let path = path.as_ref();
f(self, path)?;
for (ident, module) in &self.modules {
let path = path.join(ident);
module.write_to_files_with_impl(path, f)?;
}
Ok(())
}
}
impl ToTokens for Module {
fn to_tokens(&self, tokens: &mut TokenStream) {
self.to_code(tokens, SubModules::Inline);
}
}
fn render_usings<'x, I, X>(usings: I) -> TokenStream
where
I: IntoIterator<Item = (X, &'x bool)>,
X: AsRef<str>,
{
#[derive(Default)]
struct Module {
usings: BTreeMap<Ident2, bool>,
sub_modules: BTreeMap<Ident2, Module>,
}
impl Module {
fn render(&self) -> TokenStream {
let count = self.usings.len() + self.sub_modules.len();
let usings = self.usings.iter().map(|(ident, anonymous)| {
if *anonymous {
quote!(#ident as _)
} else {
quote!(#ident)
}
});
let sub_modules = self.sub_modules.iter().map(|(ident, module)| {
let using = module.render();
quote!(#ident::#using)
});
let items = usings.chain(sub_modules);
if count > 1 {
quote!({ #( #items ),* })
} else {
quote!(#( #items )*)
}
}
}
let mut root = Module::default();
for (using, anonymous) in usings {
let using = using.as_ref();
let Ok(ident) = IdentPath::from_str(using) else {
continue;
};
let (ident, path, _) = ident.into_parts();
let mut module = &mut root;
for part in path.into_iter().flat_map(|x| x.0) {
module = module.sub_modules.entry(part).or_default();
}
*module.usings.entry(ident).or_insert(*anonymous) &= *anonymous;
}
let mut ret = TokenStream::new();
for (ident, module) in &root.sub_modules {
let using = module.render();
ret.extend(quote!(use #ident::#using;));
}
ret
}