#![allow(dead_code)]
use proc_macro2::Span;
use quote::{quote, ToTokens};
use syn::{
parse::{Parse, ParseStream},
parse2,
punctuated::Punctuated,
spanned::Spanned,
Attribute, Error, FnArg, Ident, ImplItem, LitStr, Meta, MetaNameValue, Path, Signature, Token,
};
pub struct Route {
pub name: Ident,
pub descriptions: Vec<Attribute>,
pub sig: Signature,
pub method_args: MethodArgs,
pub attrs: Vec<Attribute>,
}
impl Route {
pub fn new(item: &mut ImplItem) -> Result<Option<Self>, Error> {
let ImplItem::Fn(item) = item else {
return Ok(None);
};
let name = item.sig.ident.clone();
if let Some(input) = item.sig.inputs.first() {
if matches!(input, FnArg::Receiver(rec) if rec.reference.is_some()) {
return Err(Error::new(
input.span(),
"Method receiver cannot be reference",
));
}
}
let mut method_args = None;
let mut attrs = vec![];
let mut descriptions: Vec<Attribute> = vec![];
for i in 0..item.attrs.len() {
let attr = item.attrs[i].clone();
if let Ok(m) = MethodType::from_path(attr.path()) {
if method_args.is_some() {
return Err(syn::Error::new(
attr.span(),
r#"Unsupported to use more than one method"#,
));
}
method_args = Some(MethodArgs::new(attr.meta, m)?);
item.attrs.remove(i);
continue;
}
if attr.path().is_ident("doc") {
descriptions.push(attr);
continue;
}
attrs.push(attr.clone());
}
let Some(method_args) = method_args else {
return Ok(None);
};
if matches!(item.sig.output, syn::ReturnType::Default) {
return Err(syn::Error::new_spanned(
item,
"Function has no return type. Cannot be used as handler (You can return no type if you add #[return_json] attribute",
));
}
if item.sig.asyncness.is_none() {
return Err(syn::Error::new_spanned(item, "Function must be async"));
}
Ok(Some(Self {
method_args,
descriptions,
sig: item.sig.clone(),
attrs,
name,
}))
}
}
impl ToTokens for Route {
fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
let Route {
method_args:
MethodArgs {
path,
guards: _,
method,
wrappers,
..
},
name,
..
} = self;
let method = Ident::new(method.as_lower_str(), Span::call_site());
let stream = if cfg!(feature = "axum") {
quote! {
route(
#path,
routing::#method(Self::#name)
#(.layer(#wrappers))*
)
}
} else {
quote!(compile_error!(
"One of the features `actix` or `axum` must be enabled"
))
};
tokens.extend(stream);
}
}
struct Args {
pub path: Option<LitStr>,
pub options: Punctuated<MetaNameValue, Token![,]>,
}
impl Parse for Args {
fn parse(input: ParseStream) -> Result<Self, Error> {
let path = input.parse::<LitStr>().ok();
if !input.peek(Token![,]) {
return Ok(Self {
path,
options: Punctuated::new(),
});
}
input.parse::<Token![,]>()?;
if input.cursor().literal().is_some() {
return Err(syn::Error::new(
Span::call_site(),
r#"Multiple paths specified! There should be only one."#,
));
}
let options = input.parse_terminated(syn::MetaNameValue::parse, Token![,])?;
Ok(Self { path, options })
}
}
pub struct MethodArgs {
pub path: syn::LitStr,
pub resource_name: Option<syn::LitStr>,
pub guards: Vec<Path>,
pub wrappers: Vec<syn::Expr>,
pub method: MethodType,
}
impl MethodArgs {
fn new(meta: Meta, method: MethodType) -> syn::Result<Self> {
let span = meta.span();
let mut path = None;
let mut resource_name = None;
let mut guards = Vec::new();
let mut wrappers = Vec::new();
match meta {
Meta::Path(_) => {}
Meta::NameValue(v) => return Err(Error::new(v.span(), "Unsupported meta")),
Meta::List(list) => {
let args = parse2::<Args>(list.tokens)?;
path = args.path;
for nv in args.options {
if nv.path.is_ident("name") {
if let syn::Expr::Lit(syn::ExprLit {
lit: syn::Lit::Str(lit),
..
}) = nv.value
{
resource_name = Some(lit);
} else {
return Err(syn::Error::new_spanned(
nv.value,
"Attribute name expects literal string",
));
}
} else if nv.path.is_ident("guard") {
if let syn::Expr::Path(syn::ExprPath { path, .. }) = nv.value {
guards.push(path);
} else {
return Err(syn::Error::new_spanned(
nv.value,
"Attribute guard expects literal string",
));
}
} else if nv.path.is_ident("wrap") {
wrappers.push(nv.value);
} else {
return Err(syn::Error::new_spanned(
nv.path,
"Unknown attribute key is specified; allowed: guard and wrap",
));
}
}
}
}
Ok(Self {
path: path
.unwrap_or_else(|| LitStr::new(if cfg!(feature = "axum") { "/" } else { "" }, span)),
resource_name,
guards,
wrappers,
method,
})
}
}
macro_rules! standard_method_type {
(
$($variant:ident, $upper:ident, $lower:ident,)+
) => {
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum MethodType {
$(
$variant,
)+
}
impl MethodType {
fn as_lower_str(&self) -> &'static str {
match self {
$(Self::$variant => stringify!($lower),)+
}
}
fn from_path(method: &Path) -> Result<Self, ()> {
match () {
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
_ => Err(()),
}
}
}
};
}
standard_method_type! {
Get, GET, get,
Post, POST, post,
Put, PUT, put,
Delete, DELETE, delete,
Head, HEAD, head,
Connect, CONNECT, connect,
Options, OPTIONS, options,
Trace, TRACE, trace,
Patch, PATCH, patch,
}