use proc_macro2::TokenStream;
use quote::quote;
use super::declaration::ApplicationDeclaration;
pub fn expand(declaration: &ApplicationDeclaration) -> TokenStream {
let graph = expand_graph_fn(declaration);
let routes = expand_routes_fn(declaration);
let page_contracts = expand_page_contracts_fn(declaration);
quote! {
#graph
#routes
#page_contracts
}
}
fn expand_graph_fn(declaration: &ApplicationDeclaration) -> TokenStream {
let visibility = &declaration.visibility;
let ident = fn_ident(declaration, "graph");
let descriptors = declaration
.modules
.iter()
.map(|path| quote! { (*#path()).clone() });
quote! {
#visibility fn #ident() -> ::std::result::Result<
::arcature::ApplicationGraph,
::arcature::GraphError,
> {
let modules: ::std::vec::Vec<::arcature::ModuleDescriptor> = ::std::vec![
#(#descriptors),*
];
::arcature::ApplicationGraph::new(modules)
}
}
}
fn expand_routes_fn(declaration: &ApplicationDeclaration) -> TokenStream {
let visibility = &declaration.visibility;
let ident = fn_ident(declaration, "routes");
let state = match &declaration.state {
Some(path) => quote! { #path },
None => quote! { () },
};
let body = match declaration.routes.split_first() {
Some((first, rest)) => {
let merges = rest.iter().map(|path| quote! { .merge(#path()) });
quote! { #first() #(#merges)* }
}
None => quote! { ::arcature::Routes::<#state>::new(::std::vec::Vec::new()) },
};
quote! {
#visibility fn #ident() -> ::arcature::Routes<#state> {
#body
}
}
}
fn expand_page_contracts_fn(declaration: &ApplicationDeclaration) -> TokenStream {
if declaration.page_contracts.is_empty() {
return TokenStream::new();
}
let visibility = &declaration.visibility;
let ident = fn_ident(declaration, "page_contracts");
let pages = &declaration.page_contracts;
quote! {
#visibility fn #ident() -> ::std::result::Result<
::arcature::inertia::PageContracts,
::arcature::inertia::ContractError,
> {
let mut registry = ::arcature::inertia::PageContracts::new();
#(
registry = registry.register_entry(&#pages::PAGE_CONTRACT_ENTRY)?;
)*
::std::result::Result::Ok(registry)
}
}
}
fn fn_ident(declaration: &ApplicationDeclaration, suffix: &str) -> syn::Ident {
syn::Ident::new(
&format!("{}_{suffix}", declaration.name.to_lowercase()),
declaration.ident.span(),
)
}
#[cfg(test)]
mod tests {
use super::*;
use quote::quote;
fn expand_application(tokens: proc_macro2::TokenStream) -> String {
let declaration: ApplicationDeclaration =
syn::parse2(tokens).expect("application should parse");
expand(&declaration).to_string()
}
#[test]
fn emits_the_graph_function_calling_each_module_accessor() {
let s = expand_application(quote! {
pub App { modules: [accounts::accounts_module, links::links_module] }
});
assert!(s.contains("fn app_graph ()"), "got: {s}");
assert!(s.contains("ApplicationGraph :: new"), "got: {s}");
assert!(s.contains("accounts :: accounts_module ()"), "got: {s}");
assert!(s.contains("links :: links_module ()"), "got: {s}");
}
#[test]
fn honours_the_declared_visibility() {
assert!(expand_application(quote! { pub App { modules: [a::m] } }).contains("pub fn"));
assert!(!expand_application(quote! { App { modules: [a::m] } }).contains("pub fn"));
}
#[test]
fn routes_default_to_an_empty_unit_state_router() {
let s = expand_application(quote! { App { modules: [a::m] } });
assert!(s.contains("Routes :: < () > :: new"), "got: {s}");
}
#[test]
fn routes_merge_in_declaration_order() {
let s = expand_application(quote! {
App {
modules: [a::m],
routes: [accounts::routes, links::routes],
state: AppState,
}
});
assert!(
s.contains("-> :: arcature :: Routes < AppState >"),
"got: {s}"
);
assert!(
s.contains("accounts :: routes () . merge (links :: routes ())"),
"got: {s}"
);
}
#[test]
fn no_page_contracts_function_without_the_section() {
let s = expand_application(quote! { App { modules: [a::m] } });
assert!(!s.contains("page_contracts"), "got: {s}");
}
#[test]
fn page_contracts_function_registers_each_entry() {
let s = expand_application(quote! {
App {
modules: [a::m],
page_contracts: [home::HomePage, links::NewLinkPage],
}
});
assert!(s.contains("fn app_page_contracts ()"), "got: {s}");
assert!(
s.contains("home :: HomePage :: PAGE_CONTRACT_ENTRY"),
"got: {s}"
);
assert!(
s.contains("links :: NewLinkPage :: PAGE_CONTRACT_ENTRY"),
"got: {s}"
);
}
}