nidus-macros 1.0.7

Proc macros for Nidus controllers, routes, modules, providers, guards, pipes, and entrypoints.
Documentation
use proc_macro2::TokenStream;
use quote::quote;
use syn::{
    Fields, GenericArgument, Ident, ItemStruct, PathArguments, Type, parse2, spanned::Spanned,
};

pub(crate) fn expand(attr: TokenStream, item: TokenStream) -> TokenStream {
    let lifetime = match injectable_lifetime(attr) {
        Ok(lifetime) => lifetime,
        Err(error) => return crate::diagnostics::compile_error_with_item(error, item),
    };

    match parse2::<ItemStruct>(item.clone()) {
        Ok(item) => expand_struct(item, lifetime),
        Err(error) => crate::diagnostics::compile_error_with_item(
            format!("#[injectable] can only be used on structs: {error}"),
            item,
        ),
    }
}

fn expand_struct(item: ItemStruct, lifetime: InjectableLifetime) -> TokenStream {
    let name = &item.ident;
    let (impl_generics, type_generics, where_clause) = item.generics.split_for_impl();
    let fields = match &item.fields {
        Fields::Named(fields) => fields,
        Fields::Unit => {
            let register_provider = unit_register_provider(&lifetime);
            return quote! {
                #item

                impl #impl_generics #name #type_generics #where_clause {
                    pub fn register_provider(
                        container: &mut ::nidus::prelude::Container,
                    ) -> ::nidus::prelude::Result<()> {
                        #register_provider
                    }
                }

                impl #impl_generics ::nidus::prelude::ProviderRegistrant for #name #type_generics #where_clause {
                    fn register_provider(
                        container: &mut ::nidus::prelude::Container,
                    ) -> ::nidus::prelude::Result<()> {
                        Self::register_provider(container)
                    }
                }
            };
        }
        Fields::Unnamed(_) => {
            return crate::diagnostics::compile_error_with_item(
                "#[injectable] currently supports named-field structs or unit structs",
                quote!(#item),
            );
        }
    };

    let resolver = Ident::new("resolver", proc_macro2::Span::call_site());
    let initializers = match field_initializers(fields, &resolver) {
        Ok(initializers) => initializers,
        Err(error) => {
            let compile_error = error.to_compile_error();
            return quote! {
                #item
                #compile_error
            };
        }
    };
    let register_provider = named_register_provider(
        &lifetime,
        &resolver,
        quote! {
            Ok(Self {
                #(#initializers,)*
            })
        },
    );

    quote! {
        #item

        impl #impl_generics #name #type_generics #where_clause {
            pub fn register_provider(
                container: &mut ::nidus::prelude::Container,
            ) -> ::nidus::prelude::Result<()> {
                #register_provider
            }
        }

        impl #impl_generics ::nidus::prelude::ProviderRegistrant for #name #type_generics #where_clause {
            fn register_provider(
                container: &mut ::nidus::prelude::Container,
            ) -> ::nidus::prelude::Result<()> {
                Self::register_provider(container)
            }
        }
    }
}

enum InjectableLifetime {
    Singleton,
    Transient,
    Request,
}

impl InjectableLifetime {
    fn provider_lifetime_tokens(&self) -> TokenStream {
        match self {
            Self::Singleton => quote!(::nidus::prelude::ProviderLifetime::Singleton),
            Self::Transient => quote!(::nidus::prelude::ProviderLifetime::Transient),
            Self::Request => quote!(::nidus::prelude::ProviderLifetime::Request),
        }
    }
}

fn unit_register_provider(lifetime: &InjectableLifetime) -> TokenStream {
    match lifetime {
        InjectableLifetime::Request => {
            quote!(container.register_request_scoped::<Self, _>(|_scope| Ok(Self)))
        }
        InjectableLifetime::Singleton | InjectableLifetime::Transient => {
            let provider_lifetime = lifetime.provider_lifetime_tokens();
            quote! {
                container.register_factory::<Self, _>(
                    #provider_lifetime,
                    |_container| Ok(Self),
                )
            }
        }
    }
}

fn named_register_provider(
    lifetime: &InjectableLifetime,
    resolver: &Ident,
    body: TokenStream,
) -> TokenStream {
    match lifetime {
        InjectableLifetime::Request => {
            quote!(container.register_request_scoped::<Self, _>(|#resolver| #body))
        }
        InjectableLifetime::Singleton | InjectableLifetime::Transient => {
            let provider_lifetime = lifetime.provider_lifetime_tokens();
            quote! {
                container.register_factory::<Self, _>(
                    #provider_lifetime,
                    |#resolver| #body,
                )
            }
        }
    }
}

fn field_initializers(
    fields: &syn::FieldsNamed,
    resolver: &Ident,
) -> syn::Result<Vec<TokenStream>> {
    fields
        .named
        .iter()
        .map(|field| {
        let ident = field.ident.as_ref().expect("named field has ident");
        match dependency_wrapper(&field.ty) {
                Some(DependencyWrapper::Inject) => Ok(quote!(#ident: #resolver.inject()?)),
                Some(DependencyWrapper::Optional) => Ok(quote!(#ident: #resolver.optional()?)),
                None => Err(syn::Error::new(
                    field.ty.span(),
                    "#[injectable] fields must use Inject<T> or Optional<T>; use an explicit factory for literal or default state",
                )),
        }
        })
        .collect()
}

fn injectable_lifetime(attr: TokenStream) -> Result<InjectableLifetime, &'static str> {
    if attr.is_empty() {
        return Ok(InjectableLifetime::Singleton);
    }

    let Ok(ident) = parse2::<Ident>(attr) else {
        return Err("#[injectable] supports no arguments, singleton, transient, or request");
    };

    match ident.to_string().as_str() {
        "singleton" => Ok(InjectableLifetime::Singleton),
        "transient" => Ok(InjectableLifetime::Transient),
        "request" => Ok(InjectableLifetime::Request),
        _ => Err("#[injectable] supports no arguments, singleton, transient, or request"),
    }
}

enum DependencyWrapper {
    Inject,
    Optional,
}

fn dependency_wrapper(ty: &Type) -> Option<DependencyWrapper> {
    let Type::Path(type_path) = ty else {
        return None;
    };
    let segment = type_path.path.segments.last()?;
    let PathArguments::AngleBracketed(arguments) = &segment.arguments else {
        return None;
    };
    let has_type_argument = arguments
        .args
        .iter()
        .any(|argument| matches!(argument, GenericArgument::Type(_)));
    if !has_type_argument {
        return None;
    }
    match segment.ident.to_string().as_str() {
        "Inject" => Some(DependencyWrapper::Inject),
        "Optional" => Some(DependencyWrapper::Optional),
        _ => None,
    }
}