use proc_macro2::TokenStream;
use quote::quote;
use syn::{
AngleBracketedGenericArguments, ImplItem, ImplItemFn, ItemImpl, ItemTrait, Path, PathArguments,
PathSegment, Type, parse2, spanned::Spanned,
};
use crate::async_rewrite;
use crate::attr_args::{AttrArgs, combine_errors};
use crate::path_prefix::{ModulePrefix, derive_module_prefix};
pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> syn::Result<TokenStream> {
if let Ok(trait_def) = parse2::<ItemTrait>(item.clone()) {
if !attr.is_empty() {
return Err(syn::Error::new(
trait_def.span(),
"#[cano::task::router]: no attribute args are accepted on a trait definition",
));
}
let rewritten = async_rewrite::rewrite_trait_def(trait_def);
return Ok(quote! { #rewritten });
}
if let Ok(item_impl) = parse2::<ItemImpl>(item.clone()) {
let args = AttrArgs::parse(attr)?;
if item_impl.trait_.is_none() {
let state_ty = args.state.ok_or_else(|| {
syn::Error::new(
item_impl.span(),
"#[cano::task::router] on an inherent `impl T { ... }` block requires \
`state = T` (e.g. `#[task::router(state = MyState)]`)",
)
})?;
return expand_inherent_impl(item_impl, state_ty, args.key);
} else {
if args.state.is_some() || args.key.is_some() {
return Err(syn::Error::new(
item_impl.span(),
"#[cano::task::router]: `state` / `key` args only apply to inherent \
`impl T { ... }` blocks; when writing `impl RouterTask<...> for T` the \
trait header already specifies them",
));
}
return expand_trait_impl(item_impl);
}
}
Err(syn::Error::new(
proc_macro2::Span::call_site(),
"#[cano::task::router]: expected a trait definition or impl block",
))
}
fn expand_trait_impl(item_impl: ItemImpl) -> syn::Result<TokenStream> {
let (state_ty, key_ty, task_trait_path, module_prefix) =
extract_state_key_task_path_and_prefix(&item_impl)?;
let router_impl = async_rewrite::rewrite_impl_block(item_impl.clone());
let task_impl = synthesise_task_impl(
&item_impl,
&state_ty,
key_ty.as_ref(),
task_trait_path,
&module_prefix,
)?;
Ok(quote! {
#router_impl
#task_impl
})
}
fn expand_inherent_impl(
item_impl: ItemImpl,
state_ty: Type,
key_ty: Option<Type>,
) -> syn::Result<TokenStream> {
let mut route_fn: Option<&ImplItemFn> = None;
let mut errors: Vec<syn::Error> = Vec::new();
let mut has_config = false;
let mut has_name = false;
for it in &item_impl.items {
match it {
ImplItem::Fn(f) => match f.sig.ident.to_string().as_str() {
"route" => route_fn = Some(f),
"config" => has_config = true,
"name" => has_name = true,
other => {
errors.push(syn::Error::new_spanned(
&f.sig.ident,
format!(
"#[cano::task::router]: unexpected method `{other}` in inherent impl; \
only `route`, `config`, and `name` are allowed"
),
));
}
},
ImplItem::Type(t) => {
errors.push(syn::Error::new_spanned(
&t.ident,
format!(
"#[cano::task::router]: unexpected associated type `{}`; \
`RouterTask` has no associated types",
t.ident
),
));
}
_ => {}
}
}
if route_fn.is_none() {
errors.push(syn::Error::new(
item_impl.span(),
"#[cano::task::router] requires an `async fn route(&self, res: &Resources<_>) \
-> Result<TaskResult<_>, CanoError>` method",
));
}
if !errors.is_empty() {
return Err(combine_errors(errors));
}
let router_trait_ref: syn::Path = match &key_ty {
Some(k) => syn::parse_quote!(::cano::RouterTask<#state_ty, #k>),
None => syn::parse_quote!(::cano::RouterTask<#state_ty>),
};
let task_trait_path: syn::Path = match &key_ty {
Some(k) => syn::parse_quote!(::cano::Task<#state_ty, #k>),
None => syn::parse_quote!(::cano::Task<#state_ty>),
};
let attrs = &item_impl.attrs;
let unsafety = &item_impl.unsafety;
let generics = &item_impl.generics;
let where_clause = &item_impl.generics.where_clause;
let self_ty = &item_impl.self_ty;
let user_items = &item_impl.items;
let config_default = if !has_config {
Some(quote! {
fn config(&self) -> ::cano::TaskConfig {
::cano::TaskConfig::default()
}
})
} else {
None
};
let name_default = if !has_name {
Some(quote! {
fn name(&self) -> ::std::borrow::Cow<'static, str> {
::std::borrow::Cow::Borrowed(::std::any::type_name::<Self>())
}
})
} else {
None
};
let synth = quote! {
#(#attrs)*
#unsafety impl #generics #router_trait_ref for #self_ty #where_clause {
#config_default
#name_default
#(#user_items)*
}
};
let synth_impl: ItemImpl = parse2(synth)?;
let router_impl = async_rewrite::rewrite_impl_block(synth_impl.clone());
let module_prefix = ModulePrefix::Cano;
let task_impl = synthesise_task_impl(
&synth_impl,
&state_ty,
key_ty.as_ref(),
task_trait_path,
&module_prefix,
)?;
Ok(quote! {
#router_impl
#task_impl
})
}
fn extract_state_key_task_path_and_prefix(
item_impl: &ItemImpl,
) -> syn::Result<(Type, Option<Type>, Path, ModulePrefix)> {
let (_, trait_path, _) = item_impl
.trait_
.as_ref()
.ok_or_else(|| syn::Error::new(item_impl.span(), "expected a trait impl block"))?;
let last_seg = trait_path
.segments
.last()
.ok_or_else(|| syn::Error::new(item_impl.span(), "cannot read trait path segments"))?;
let args = match &last_seg.arguments {
syn::PathArguments::AngleBracketed(a) => a,
_ => {
return Err(syn::Error::new(
item_impl.span(),
"RouterTask impl must have angle-bracketed type arguments (e.g. `RouterTask<MyState>`)",
));
}
};
let type_args: Vec<&Type> = args
.args
.iter()
.filter_map(|a| {
if let syn::GenericArgument::Type(t) = a {
Some(t)
} else {
None
}
})
.collect();
if type_args.is_empty() {
return Err(syn::Error::new(
item_impl.span(),
"RouterTask impl requires at least one type argument (the state type)",
));
}
let state_ty = type_args[0].clone();
let key_ty = type_args.get(1).map(|t| (*t).clone());
let module_prefix = derive_module_prefix(trait_path);
let task_path = derive_task_path_from_router_path(trait_path, &state_ty, key_ty.as_ref())?;
Ok((state_ty, key_ty, task_path, module_prefix))
}
fn derive_task_path_from_router_path(
router_path: &Path,
state_ty: &Type,
key_ty: Option<&Type>,
) -> syn::Result<Path> {
let mut task_path = router_path.clone();
let angle_args: AngleBracketedGenericArguments = match key_ty {
Some(k) => syn::parse_quote!(<#state_ty, #k>),
None => syn::parse_quote!(<#state_ty>),
};
let task_args = PathArguments::AngleBracketed(angle_args);
if let Some(last) = task_path.segments.last_mut() {
*last = PathSegment {
ident: syn::Ident::new("Task", last.ident.span()),
arguments: task_args,
};
}
Ok(task_path)
}
fn synthesise_task_impl(
router_impl: &ItemImpl,
state_ty: &Type,
key_ty: Option<&Type>,
task_trait_path: Path,
module_prefix: &ModulePrefix,
) -> syn::Result<TokenStream> {
let attrs = &router_impl.attrs;
let generics = &router_impl.generics;
let where_clause = &router_impl.generics.where_clause;
let self_ty = &router_impl.self_ty;
let (_, router_trait_path, _) = router_impl.trait_.as_ref().ok_or_else(|| {
syn::Error::new(router_impl.span(), "expected a RouterTask trait impl block")
})?;
let task_config_ty = module_prefix.qualify("TaskConfig");
let resources_ty = module_prefix.qualify("Resources");
let task_result_ty = module_prefix.qualify("TaskResult");
let cano_error_ty = module_prefix.qualify("CanoError");
let key_ty_tok: TokenStream = match key_ty {
Some(k) => quote! { #k },
None => quote! { ::std::borrow::Cow<'static, str> },
};
let synth = quote! {
#(#attrs)*
impl #generics #task_trait_path for #self_ty #where_clause {
fn config(&self) -> #task_config_ty {
<Self as #router_trait_path>::config(self)
}
fn name(&self) -> ::std::borrow::Cow<'static, str> {
<Self as #router_trait_path>::name(self)
}
async fn run(
&self,
res: &#resources_ty<#key_ty_tok>,
) -> ::std::result::Result<#task_result_ty<#state_ty>, #cano_error_ty> {
<Self as #router_trait_path>::route(self, res).await
}
}
};
let synth_impl: ItemImpl = parse2(synth)?;
Ok(async_rewrite::rewrite_impl_block(synth_impl))
}