ngyn_macros 0.4.3

Modular backend framework for web applications
Documentation
use proc_macro::TokenStream;
use quote::quote;

use crate::utils::parse_macro_data::parse_macro_data;

struct InjectableArgs {
    init: Option<syn::LitStr>,
    inject: Option<syn::LitStr>,
}

impl syn::parse::Parse for InjectableArgs {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let mut init = None;
        let mut inject = None;

        while !input.is_empty() {
            let ident: syn::Ident = input.parse()?;
            input.parse::<syn::Token![=]>()?;

            match ident.to_string().as_str() {
                "init" => {
                    init = input.parse()?;
                }
                "inject" => {
                    inject = input.parse()?;
                }
                _ => {
                    return Err(syn::Error::new(
                        ident.span(),
                        format!("unexpected attribute `{}`", ident),
                    ));
                }
            }
        }

        Ok(InjectableArgs { init, inject })
    }
}

pub(crate) fn injectable_macro(args: TokenStream, input: TokenStream) -> TokenStream {
    let syn::DeriveInput {
        ident,
        data,
        attrs,
        vis,
        generics,
    } = syn::parse_macro_input!(input as syn::DeriveInput);
    let InjectableArgs { init, inject } = syn::parse_macro_input!(args as InjectableArgs);
    let injectable_fields = parse_macro_data(data);

    let generics_params = if generics.params.iter().count() > 0 {
        let generics_params = generics.params.iter().map(|param| {
            if let syn::GenericParam::Type(ty) = param {
                let ident = &ty.ident;
                quote! { #ident }
            } else {
                quote! { #param }
            }
        });
        quote! {
            <#(#generics_params),*>
        }
    } else {
        quote! {}
    };

    let mut add_fields = Vec::new();
    let mut inject_fields = Vec::new();
    let fields: Vec<_> = injectable_fields
        .iter()
        .map(
            |syn::Field {
                 ident,
                 ty,
                 vis,
                 attrs,
                 colon_token,
                 ..
             }| {
                add_fields.push(quote! {
                    #ident #colon_token Default::default()
                });
                if attrs.iter().any(|attr| attr.path().is_ident("inject")) {
                    inject_fields.push(quote! {
                        self.#ident.inject(cx);
                    });
                }
                let attrs = attrs.iter().filter(|attr| !attr.path().is_ident("inject"));
                quote! {
                    #(#attrs),* #vis #ident #colon_token #ty
                }
            },
        )
        .collect();

    let init_injectable = match init {
        Some(init) => {
            let init_ident = init.parse::<syn::Ident>().unwrap();
            quote! {
                #ident::#init_ident()
            }
        }
        None => quote! {
            #ident {
                #(#add_fields),*
            }
        },
    };

    let inject_injectable = match inject {
        Some(inject) => {
            let inject_ident = inject.parse::<syn::Ident>().unwrap();
            quote! {
                #ident::#inject_ident(cx)
            }
        }
        None => quote! {},
    };

    let expanded = quote! {
        #(#attrs)*
        #vis struct #ident #generics {
            #(#fields),*
        }

        impl #generics ngyn::prelude::NgynInjectable for #ident #generics_params {
            fn new() -> Self {
                #init_injectable
            }

            fn inject(&mut self, cx: &ngyn::prelude::NgynContext) {
                #(#inject_fields)*
                #inject_injectable
            }
        }

        impl #generics Default for #ident #generics_params {
            fn default() -> Self {
                Self::new()
            }
        }
    };
    expanded.into()
}