use std::collections::HashMap;
use proc_macro2::{Span, TokenStream};
use quote::quote;
use quote::ToTokens;
use syn::ExprArray;
use syn::PatType;
use syn::{ItemFn, Token, ExprAssign, Expr, Ident, PathSegment, parse_quote, Type, FnArg, PathArguments, GenericArgument};
use syn::parse::{Parse, ParseStream, Result};
macro_rules! props {
($($o: ident),+$(,)?) => {
#[allow(non_camel_case_types)]
#[derive(Clone, Debug)]
enum Prop {
$($o),+,
Extra(&'static str),
}
impl From<String> for Prop {
fn from(value: String) -> Self {
match value.as_str() {
$(stringify!($o) => Prop::$o),+,
_ => Prop::Extra(Box::leak(value.into_boxed_str())),
}
}
}
};
}
props!(path, methods, suffixes, omission, id, responsive, redirect, name);
const ORDERED_PROPS: [Prop; 8] = [Prop::path, Prop::suffixes, Prop::omission, Prop::id, Prop::responsive, Prop::redirect, Prop::name, Prop::methods];
pub struct Api {
pub path: Option<Expr>,
pub methods: Option<Expr>,
pub suffixes: Option<Expr>,
pub omission: Option<Expr>,
pub id: Option<Expr>,
pub responsive: Option<Expr>,
pub redirect: Option<Expr>,
pub name: Option<Expr>,
pub extras: HashMap<&'static str, Expr>,
}
impl Api {
fn set_by_key(&mut self, key: Prop, expr: Expr) -> Result<()> {
const CONSUME_IF: fn(bool, &mut dyn FnMut()) -> bool = |result, callback| {
if result { callback(); }
result
};
if ! match &key {
Prop::path => CONSUME_IF(self.path.is_none(), &mut || self.path = Some(expr.clone())),
Prop::methods => CONSUME_IF(self.methods.is_none(), &mut || self.methods = Some(expr.clone())),
Prop::suffixes => CONSUME_IF(self.suffixes.is_none(), &mut || self.suffixes = Some(expr.clone())),
Prop::id => CONSUME_IF(self.id.is_none(), &mut || self.id = Some(expr.clone())),
Prop::omission => CONSUME_IF(self.omission.is_none(), &mut || self.omission = Some(expr.clone())),
Prop::responsive => CONSUME_IF(self.responsive.is_none(), &mut || self.responsive = Some(expr.clone())),
Prop::redirect => CONSUME_IF(self.redirect.is_none(), &mut || self.redirect = Some(expr.clone())),
Prop::name => CONSUME_IF(self.name.is_none(), &mut || self.name = Some(expr.clone())),
Prop::Extra(key) => CONSUME_IF(! self.extras.contains_key(key), &mut || {self.extras.insert(key, expr.clone()); ()}),
} {
unreachable!(r#"Api arg "{:?}" can only set once"#, key);
}
Ok(())
}
fn get_rp_generic(func: &ItemFn) -> &Type {
if let Some(FnArg::Typed(PatType{ty, ..})) = func.sig.inputs.first() {
if let Type::Path(tp) = ty.as_ref() {
if let Some(PathSegment{arguments: PathArguments::AngleBracketed(args), ..}) = tp.path.segments.last() {
if let Some(GenericArgument::Type(lt)) = args.args.last() {
return lt
}
}
}
}
unreachable!()
}
pub fn processing(self, input: ItemFn) -> TokenStream {
let fn_ident = input.sig.ident.clone();
let fn_name = fn_ident.to_string();
let api_fn_name = Ident::new(format!("{}_api", fn_name.as_str()).as_str(), Span::call_site());
let Self{path, methods, suffixes, omission, id, responsive, redirect, name, extras} = self;
let path = path.unwrap_or_else(|| parse_quote!(#fn_name));
let methods = methods.unwrap_or_else(|| parse_quote! {dce_router::api::Methods::default()});
let suffixes = suffixes.map(|ss| match ss {
Expr::Array(ExprArray{elems, ..}) => {
let exprs: Vec<Expr> = elems.into_iter().map(|e| {syn::parse_quote! {dce_router::api::Suffix(#e)}}).collect();
syn::parse_quote! {vec![#(#exprs),*]}
},
v => v,
}).unwrap_or_else(|| parse_quote! {vec![]});
let id: Expr = id.map(|r| parse_quote!(Some(#r))).unwrap_or_else(|| parse_quote!(None));
let omission = omission.unwrap_or_else(|| parse_quote!(false));
let redirect: Expr = redirect.map(|r| parse_quote!(Some(#r))).unwrap_or_else(|| parse_quote!(None));
let name = name.unwrap_or_else(|| {
let p = path.clone().into_token_stream().to_string().trim_matches('"').to_string();
let n = &p.as_str()[p.rfind('/').map_or(0, |i| i + 1)..];
parse_quote!(#n)
});
let responsive = responsive.unwrap_or_else(|| parse_quote!(true));
let extras: Vec<_> = extras.into_iter().map(|(k, v)| quote!((#k, Box::new(#v) as Box<dyn std::any::Any + Send + Sync>))).collect();
let extras = quote! { std::collections::HashMap::from_iter([#(#extras),*]) };
let rp_generic = Self::get_rp_generic(&input);
let controller: Expr = if input.sig.asyncness.is_none() {
parse_quote!(Some(dce_router::api::Handler::Sync(#fn_ident)))
} else {
parse_quote!(Some(dce_router::api::Handler::Async(Box::new(|r| Box::pin(#fn_ident(r))))))
};
TokenStream::from(quote!(
#input
pub fn #api_fn_name() -> dce_router::api::Api<#rp_generic> {
dce_router::api::Api::new_with(
#path,
#methods,
#suffixes,
#omission,
#id,
#responsive,
#redirect,
#name,
#extras,
#controller,
)
}
))
}
}
impl Parse for Api {
fn parse(input: ParseStream) -> Result<Self> {
let mut api = Api {
path: None,
suffixes: None,
omission: None,
id: None,
responsive: None,
redirect: None,
name: None,
methods: None,
extras: Default::default(),
};
let mut prop_index = 0;
while let Ok(expr) = input.parse() {
match expr {
Expr::Assign(ExprAssign{left, right, ..}) => match *left {
Expr::Path(expr) => api.set_by_key(Prop::from(expr.path.get_ident().map_or_else(|| unreachable!(), ToString::to_string)), *right)?,
_ => unreachable!("Arg name of api was invalid"),
},
expr => {
api.set_by_key(ORDERED_PROPS.get(prop_index).unwrap_or_else(|| panic!(r#"Api argument index "{}" was invalid"#, prop_index)).clone(), expr)?;
prop_index += 1;
},
}
if input.parse::<Token![,]>().is_err() { break }
}
Ok(api)
}
}