dce-macro 1.8.0

The macros for the dce-router lib
Documentation
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())),
            // put all non-standard meta into extras mapping
            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)
    }
}