ngyn_macros 0.3.1

Modular backend framework for web applications
Documentation
use ngyn_shared::{HttpMethod, Path};
use proc_macro::TokenStream;
use quote::quote;
use syn::ItemImpl;

pub struct PathArg {
    pub path: Option<Path>,
}

impl syn::parse::Parse for PathArg {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let path = if input.peek(syn::LitStr) {
            let path = input.parse::<syn::LitStr>()?;
            Some(Path::Single(path.value()))
        } else if input.peek(syn::token::Bracket) {
            let content;
            syn::bracketed!(content in input);
            let mut paths = Vec::new();
            while !content.is_empty() {
                let path = content.parse::<syn::LitStr>()?;
                paths.push(path.value());
                if !content.is_empty() {
                    content.parse::<syn::Token![,]>()?;
                }
            }
            Some(Path::Multiple(paths))
        } else {
            None
        };

        Ok(PathArg { path })
    }
}

pub struct Args {
    pub path: Option<Path>,
    pub http_method: String,
}

impl syn::parse::Parse for Args {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let http_method = input.parse::<syn::LitStr>()?.value();

        if HttpMethod::from(&http_method) == HttpMethod::Unknown {
            panic!("Unsupported HTTP method: {}", http_method)
        } else {
            input.parse::<syn::Token![,]>()?;
        }

        let PathArg { path } = input.parse::<PathArg>()?;

        Ok(Args { path, http_method })
    }
}

pub fn routes_macro(raw_input: TokenStream) -> TokenStream {
    let ItemImpl {
        items,
        attrs,
        defaultness,
        unsafety,
        impl_token,
        generics,
        trait_,
        self_ty,
        ..
    } = match syn::parse::<ItemImpl>(raw_input) {
        Ok(input) => input,
        Err(_err) => panic!("Only impl blocks are supported"),
    };

    match trait_ {
        Some((..)) => panic!("Trait impls are not supported"),
        None => quote! {},
    };

    let mut route_defs: Vec<_> = Vec::new();
    let mut handle_routes: Vec<_> = Vec::new();

    for item in items.clone() {
        if let syn::ImplItem::Fn(method) = item {
            for attr in method.attrs.clone() {
                if attr.path().is_ident("route")
                    || attr.path().is_ident("get")
                    || attr.path().is_ident("post")
                    || attr.path().is_ident("put")
                    || attr.path().is_ident("delete")
                    || attr.path().is_ident("patch")
                    || attr.path().is_ident("head")
                    || attr.path().is_ident("options")
                {
                    let (path, http_method) = {
                        if attr.path().is_ident("route") {
                            let Args { path, http_method } = attr.parse_args::<Args>().unwrap();
                            (path, http_method)
                        } else {
                            let PathArg { path } = attr.parse_args::<PathArg>().unwrap();
                            let http_method = attr.path().get_ident().unwrap().to_string();
                            (path, http_method)
                        }
                    };
                    path.unwrap().each(|path| {
                        let ident = method.sig.ident.clone();
                        let ident_str = ident.to_string();
                        route_defs.push(quote! {(
                            #path,
                            #http_method,
                            #ident_str,
                        )});
                        handle_routes.push(quote! {
                            #ident_str => {
                                let body = self.#ident(req, res).await;
                                res.peek(body);
                            }
                        });
                    })
                }
            }
        }
    }

    let expanded = quote! {
        #defaultness #unsafety #(#attrs)*
        #impl_token #generics #self_ty {
            #[allow(non_upper_case_globals)]
            const routes: &'static [(&'static str, &'static str, &'static str)] = &[
                #(#route_defs),*
            ];
            #(#items)*

            async fn __handle_route(
                &self,
                handler: &str,
                req: &mut ngyn::prelude::NgynRequest,
                res: &mut ngyn::prelude::NgynResponse
            ) {
                match handler {
                    #(#handle_routes),*
                    _ => {
                        res.set_status(404);
                    }
                }
            }
        }
    };

    expanded.into()
}