rust-dix-macros 0.6.0

Procedural macros for rust-dix — a Rust dependency injection framework
Documentation
use proc_macro2::TokenStream as TokenStream2;
use quote::{format_ident, quote};
use syn::{
    parse::{Parse, ParseStream},
    ItemImpl,
};

use crate::field_init::gen_field_init;
use crate::module::LT;

pub(crate) fn lt_to_token(lt: LT) -> TokenStream2 {
    match lt {
        LT::S => quote! { rust_dix::ServiceLifetime::Singleton },
        LT::Sc => quote! { rust_dix::ServiceLifetime::Scoped },
        LT::T => quote! { rust_dix::ServiceLifetime::Transient },
    }
}

pub(crate) struct InjectArgs {
    pub lifetime: Option<LT>,
}

impl Parse for InjectArgs {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        if input.is_empty() {
            return Ok(InjectArgs { lifetime: None });
        }
        let lt: LT = input.parse()?;
        Ok(InjectArgs { lifetime: Some(lt) })
    }
}

pub(crate) fn expand_inject(
    args: InjectArgs,
    item: syn::Item,
) -> syn::Result<TokenStream2> {
    let lifetime = args.lifetime.unwrap_or(LT::S);
    match &item {
        syn::Item::Struct(s) => expand_inject_struct(s, lifetime),
        syn::Item::Impl(i) => expand_inject_impl(i, lifetime),
        _ => Err(syn::Error::new_spanned(
            &item,
            "#[inject] can only be placed on a struct or a trait impl block",
        )),
    }
}

fn expand_inject_struct(
    struct_item: &syn::ItemStruct,
    lifetime: LT,
) -> syn::Result<TokenStream2> {
    let name = &struct_item.ident;
    let fn_name = format_ident!("__rdi_construct_{}", name);
    let factory_name = format_ident!("__rdi_factory_{}", name);
    let type_name_fn_name = format_ident!("__rdi_type_name_{}", name);

    let constructor_body = match &struct_item.fields {
        syn::Fields::Named(n) => {
            let mut inits = Vec::new();
            for field in n.named.iter() {
                inits.push(gen_field_init(field)?);
            }
            quote! { #name { #(#inits),* } }
        }
        syn::Fields::Unit => quote! { #name },
        _ => {
            return Err(syn::Error::new_spanned(
                name,
                "named struct or unit struct required",
            ))
        }
    };

    let constructor = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        pub fn #fn_name(resolver: &dyn rust_dix::IServiceResolver) -> ::std::sync::Arc<#name> {
            ::std::sync::Arc::new(#constructor_body)
        }
    };

    let lt_ident = lt_to_token(lifetime);

    let factory = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        fn #factory_name(resolver: &dyn rust_dix::IServiceResolver) -> ::std::sync::Arc<dyn ::std::any::Any + ::std::marker::Send + ::std::marker::Sync> {
            let v: ::std::sync::Arc<#name> = #fn_name(resolver);
            ::std::sync::Arc::new(v)
                as ::std::sync::Arc<dyn ::std::any::Any + ::std::marker::Send + ::std::marker::Sync>
        }
    };

    let type_name_helper = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        fn #type_name_fn_name() -> &'static str {
            ::std::any::type_name::<#name>()
        }
    };

    let submission = quote! {
        rust_dix::inventory::submit! {
            rust_dix::ServiceRegistration {
                lifetime: #lt_ident,
                type_id: ::std::any::TypeId::of::<#name>(),
                type_name_fn: #type_name_fn_name,
                factory: #factory_name,
            }
        }
    };

    let mut stripped = struct_item.clone();
    if let syn::Fields::Named(n) = &mut stripped.fields {
        for field in n.named.iter_mut() {
            field.attrs.retain(|a| !a.path().is_ident("inject"));
        }
    }

    Ok(quote! {
        #stripped
        #constructor
        #type_name_helper
        #factory
        #submission
    })
}

fn expand_inject_impl(
    impl_item: &ItemImpl,
    lifetime: LT,
) -> syn::Result<TokenStream2> {
    let (not, trait_path, _) = impl_item.trait_.as_ref().ok_or_else(|| {
        syn::Error::new_spanned(
            impl_item,
            "#[inject] on an impl block requires a trait impl: use `impl Trait for Type`",
        )
    })?;
    if not.is_some() {
        return Err(syn::Error::new_spanned(
            impl_item,
            "#[inject] does not support negative impls",
        ));
    }

    let impl_ty = &impl_item.self_ty;
    let type_ident = match &**impl_ty {
        syn::Type::Path(tp) => tp.path.segments.last().map(|s| s.ident.clone()),
        _ => None,
    }
    .ok_or_else(|| syn::Error::new_spanned(impl_ty, "expected a path type"))?;

    let trait_segment = trait_path.segments.last().unwrap();
    let trait_suffix = &trait_segment.ident;

    let constructor_fn = format_ident!("__rdi_construct_{}", type_ident);
    let factory_name = format_ident!("__rdi_factory_{}_for_{}", type_ident, trait_suffix);
    let type_name_fn_name = format_ident!("__rdi_type_name_{}_for_{}", type_ident, trait_suffix);

    let lt_ident = lt_to_token(lifetime);
    let trait_ty = quote! { dyn #trait_path };

    let factory = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        fn #factory_name(resolver: &dyn rust_dix::IServiceResolver) -> ::std::sync::Arc<dyn ::std::any::Any + ::std::marker::Send + ::std::marker::Sync> {
            let v: ::std::sync::Arc<#impl_ty> = #constructor_fn(resolver);
            let v2: ::std::sync::Arc<#trait_ty> = v;
            ::std::sync::Arc::new(v2)
                as ::std::sync::Arc<dyn ::std::any::Any + ::std::marker::Send + ::std::marker::Sync>
        }
    };

    let type_name_helper = quote! {
        #[doc(hidden)]
        #[allow(non_snake_case)]
        fn #type_name_fn_name() -> &'static str {
            ::std::any::type_name::<#trait_ty>()
        }
    };

    let submission = quote! {
        rust_dix::inventory::submit! {
            rust_dix::ServiceRegistration {
                lifetime: #lt_ident,
                type_id: ::std::any::TypeId::of::<#trait_ty>(),
                type_name_fn: #type_name_fn_name,
                factory: #factory_name,
            }
        }
    };

    Ok(quote! {
        #impl_item
        #type_name_helper
        #factory
        #submission
    })
}