use proc_macro2::TokenStream;
use quote::quote;
use super::declaration::ModuleDeclaration;
use super::schedule_spec::ScheduleSpec;
pub fn expand(declaration: &ModuleDeclaration) -> TokenStream {
let ModuleDeclaration {
visibility,
name,
ident,
imports,
exports,
controllers,
services,
policies,
..
} = declaration;
let accessor = syn::Ident::new(&format!("{}_module", name.to_lowercase()), ident.span());
let controller_methods = controllers.iter().map(|controller| {
let controller = syn::Ident::new(controller, ident.span());
quote! { <#controller as ::arcature::ControllerMetadata>::METHODS }
});
let routes = match &declaration.routes {
Some(path) => quote! { #path },
None => quote! { &[] },
};
let listeners = declaration.listeners.iter().map(|(event, listener)| {
quote! { ::arcature::ListenerBinding { event: #event, listener: #listener } }
});
let jobs = declaration.jobs.iter().map(|(kind, version, handler)| {
quote! { ::arcature::JobBinding { kind: #kind, version: #version, handler: #handler } }
});
let commands = declaration.commands.iter().map(|(name, function)| {
quote! { ::arcature::CommandBinding { name: #name, function: #function } }
});
let schedules = declaration
.schedules
.iter()
.map(|(kind, version, cadence)| {
let cadence = match cadence {
ScheduleSpec::Every { seconds } => {
quote! { ::arcature::ScheduleCadence::Every { seconds: #seconds } }
}
ScheduleSpec::Daily { hour, minute } => {
quote! { ::arcature::ScheduleCadence::Daily { hour: #hour, minute: #minute } }
}
};
quote! {
::arcature::ScheduleBinding {
job: #kind,
version: #version,
cadence: #cadence,
}
}
});
let pages = declaration.pages.iter().map(|path| {
quote! { #path::PAGE_CONTRACT_ENTRY.name }
});
let doc = format!("Returns the module descriptor for the `{name}` module.");
quote! {
#visibility const #ident: ::arcature::ModuleDescriptor =
::arcature::ModuleDescriptor {
name: #name,
imports: &[#(#imports),*],
exports: &[#(#exports),*],
controllers: &[#(#controllers),*],
controller_methods: &[#(#controller_methods),*],
services: &[#(#services),*],
policies: &[#(#policies),*],
routes: #routes,
listeners: &[#(#listeners),*],
jobs: &[#(#jobs),*],
commands: &[#(#commands),*],
schedules: &[#(#schedules),*],
pages: &[#(#pages),*],
};
#[doc = #doc]
#visibility fn #accessor() -> &'static ::arcature::ModuleDescriptor {
&#ident
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
fn expand_module(tokens: proc_macro2::TokenStream) -> String {
let declaration: ModuleDeclaration = syn::parse2(tokens).expect("module should parse");
expand(&declaration).to_string()
}
#[test]
fn emits_the_descriptor_const_and_accessor() {
let s = expand_module(quote! { pub Accounts {} });
assert!(s.contains("const ACCOUNTS_MODULE"), "got: {s}");
assert!(s.contains(":: arcature :: ModuleDescriptor"), "got: {s}");
assert!(s.contains("fn accounts_module ()"), "got: {s}");
assert!(s.contains("\"Accounts\""), "got: {s}");
}
#[test]
fn honours_the_declared_visibility() {
assert!(expand_module(quote! { pub Accounts {} }).contains("pub const"));
assert!(!expand_module(quote! { Accounts {} }).contains("pub const"));
}
#[test]
fn emits_controller_names_and_their_method_metadata() {
let s = expand_module(quote! { A { controllers: [HomeController] } });
assert!(s.contains("\"HomeController\""), "got: {s}");
assert!(
s.contains("< HomeController as :: arcature :: ControllerMetadata > :: METHODS"),
"got: {s}"
);
}
#[test]
fn routes_defaults_to_an_empty_slice_and_uses_the_named_const() {
assert!(expand_module(quote! { A {} }).contains("routes : & []"));
assert!(expand_module(quote! { A { routes: A_ROUTES } }).contains("routes : A_ROUTES"));
}
#[test]
fn emits_listener_job_and_command_bindings() {
let s = expand_module(quote! {
A {
listeners: [UserRegistered => send_welcome],
jobs: [send_email v2 => handle],
commands: ["users:prune" => prune],
}
});
assert!(s.contains("ListenerBinding"), "got: {s}");
assert!(s.contains("\"UserRegistered\""), "got: {s}");
assert!(s.contains("JobBinding"), "got: {s}");
assert!(s.contains("version : 2i16"), "got: {s}");
assert!(s.contains("CommandBinding"), "got: {s}");
assert!(s.contains("\"users:prune\""), "got: {s}");
}
#[test]
fn reads_each_page_identity_off_its_contract_entry() {
let s = expand_module(quote! { A { pages: [HomePage, pages::NewLinkPage] } });
assert!(
s.contains("HomePage :: PAGE_CONTRACT_ENTRY . name"),
"got: {s}"
);
assert!(
s.contains("pages :: NewLinkPage :: PAGE_CONTRACT_ENTRY . name"),
"got: {s}"
);
}
#[test]
fn pages_defaults_to_an_empty_slice() {
assert!(expand_module(quote! { A {} }).contains("pages : & []"));
}
#[test]
fn emits_both_schedule_cadences() {
let s = expand_module(quote! {
A { schedules: [sweep every "5m", digest daily "03:30"] }
});
assert!(s.contains("Every { seconds : 300u64 }"), "got: {s}");
assert!(
s.contains("Daily { hour : 3u8 , minute : 30u8 }"),
"got: {s}"
);
}
}