use crate::error::{Error, Result};
use proc_macro::TokenStream;
use proc_macro2::{Ident, Span, TokenStream as TokenStream2};
use quote::{format_ident, quote};
use std::env;
use std::ffi::OsStr;
use std::fs;
use std::path::{Path, PathBuf};
use syn::parse::{Parse, ParseStream};
use syn::{parse_macro_input, LitStr, ParenthesizedGenericArguments, Token};
mod error;
struct DokoArgs {
path: LitStr,
method: Ident,
signature: ParenthesizedGenericArguments,
}
impl Parse for DokoArgs {
fn parse(input: ParseStream) -> syn::Result<Self> {
let path = input.parse()?;
input.parse::<Token![,]>()?;
let method_name: LitStr = input.parse()?;
input.parse::<Token![,]>()?;
let signature = input.parse()?;
Ok(DokoArgs {
path,
method: Ident::new(&format!("{}", method_name.value()), Span::call_site()),
signature,
})
}
}
struct SubmoduleData {
ident: Ident,
name: String,
include: TokenStream2,
}
#[proc_macro]
pub fn doko(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DokoArgs);
match tokens_for_input(input, true) {
Ok(tokens) => tokens,
Err(err) => TokenStream::from(syn::Error::new(Span::call_site(), err).to_compile_error()),
}
}
#[proc_macro]
pub fn doko_skip_mods(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DokoArgs);
match tokens_for_input(input, false) {
Ok(tokens) => tokens,
Err(err) => TokenStream::from(syn::Error::new(Span::call_site(), err).to_compile_error()),
}
}
fn tokens_for_input(input: DokoArgs, include_mods: bool) -> Result<TokenStream> {
let enclosing_modules = get_enclosing_modules(&input.path.value())?;
let submod_data = get_submodule_data(&input.path.value())?;
let outer_mod = build_module_includes(&submod_data, &enclosing_modules);
let registry = build_registry(
&submod_data,
&enclosing_modules,
&input.method,
&input.signature,
);
if include_mods {
Ok(TokenStream::from(quote! {
#outer_mod
#registry
}))
} else {
Ok(TokenStream::from(quote! {
#registry
}))
}
}
fn get_enclosing_modules<P: AsRef<Path>>(directory: &P) -> Result<Vec<Ident>> {
directory
.as_ref()
.components()
.skip(1) .map(|section| {
let section_name = section
.as_os_str()
.to_str()
.ok_or(Error::Utf8(section.as_os_str().to_os_string()))?;
Ok(Ident::new(section_name, Span::call_site()))
})
.collect()
}
fn get_submodule_data<P: AsRef<Path> + AsRef<OsStr>>(directory: &P) -> Result<Vec<SubmoduleData>> {
let dir = match env::var_os("CARGO_MANIFEST_DIR") {
Some(manifest_dir) => PathBuf::from(manifest_dir).join(directory),
None => PathBuf::from(directory),
};
Ok(source_file_names(dir)?
.into_iter()
.map(|name| data_for_submodule(name))
.collect())
}
fn data_for_submodule(name: String) -> SubmoduleData {
if name.contains('-') {
let path = format!("{}.rs", name);
let name = name.replace('-', "_").to_string();
let ident = Ident::new(&name.replace('-', "_"), Span::call_site());
SubmoduleData {
ident: ident.clone(),
name,
include: quote! {
#[path = #path]
pub mod #ident;
},
}
} else {
let ident = Ident::new(&name, Span::call_site());
SubmoduleData {
ident: ident.clone(),
name: name.to_string(),
include: quote! {
pub mod #ident;
},
}
}
}
fn build_module_includes(
submodules: &Vec<SubmoduleData>,
enclosing_modules: &Vec<Ident>,
) -> TokenStream2 {
let inner_modules =
TokenStream2::from_iter(submodules.iter().map(|submod| submod.include.clone()));
enclosing_modules
.iter()
.rev()
.fold(inner_modules, |stream, module| {
TokenStream2::from(quote!( pub mod #module { #stream } ))
})
}
fn build_registry(
submodules: &Vec<SubmoduleData>,
enclosing_modules: &Vec<Ident>,
method: &Ident,
signature: &ParenthesizedGenericArguments,
) -> TokenStream2 {
let gen_method_name = format_ident!("doko_{}", method);
let args = &signature.inputs;
let return_type = &signature.output;
let prefix = enclosing_modules
.iter()
.fold(quote! { crate }, |ident, next| quote! { #ident::#next });
let calls = TokenStream2::from_iter(
submodules
.iter()
.map(|submod| get_call_for_submodule(submod, &prefix, method)),
);
TokenStream2::from(quote!(
pub fn #gen_method_name(module_name: &str) -> fn(#args) #return_type {
match module_name {
#calls
unknown => panic!("unknown module: {}", unknown),
}
}
))
}
fn get_call_for_submodule(
submod: &SubmoduleData,
prefix: &TokenStream2,
method: &Ident,
) -> TokenStream2 {
let ident = &submod.ident;
let key = LitStr::new(&submod.name, Span::call_site());
quote! {
#key => #prefix::#ident::#method,
}
}
fn source_file_names<P: AsRef<Path>>(dir: P) -> Result<Vec<String>> {
let mut names = Vec::new();
let mut failures = Vec::new();
for entry in fs::read_dir(dir)? {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
let file_name = entry.file_name();
if file_name == "mod.rs" || file_name == "lib.rs" || file_name == "main.rs" {
continue;
}
let path = Path::new(&file_name);
if path.extension() == Some(OsStr::new("rs")) {
match file_name.into_string() {
Ok(mut utf8) => {
utf8.truncate(utf8.len() - ".rs".len());
names.push(utf8);
}
Err(non_utf8) => {
failures.push(non_utf8);
}
}
}
}
failures.sort();
if let Some(failure) = failures.into_iter().next() {
return Err(Error::Utf8(failure));
}
if names.is_empty() {
return Err(Error::Empty);
}
names.sort();
Ok(names)
}