use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Fields, FieldsNamed};
fn has_inject_attr(field: &syn::Field) -> bool {
field
.attrs
.iter()
.any(|attr| attr.path().is_ident("inject"))
}
pub fn injectable_impl(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let ferro = quote!(::ferro);
let name = &input.ident;
let name_str = name.to_string();
let vis = &input.vis;
let attrs = &input.attrs;
let generics = &input.generics;
let expanded = match &input.data {
syn::Data::Struct(data_struct) => {
match &data_struct.fields {
Fields::Named(fields_named) => generate_for_named_struct(
&ferro,
name,
name_str,
vis,
attrs,
generics,
fields_named,
),
Fields::Unit => {
quote! {
#(#attrs)*
#[derive(Default, Clone)]
#vis struct #name #generics;
#ferro::inventory::submit! {
#ferro::container::provider::SingletonEntry {
register: || {
#ferro::App::singleton(<#name as ::std::default::Default>::default());
},
name: #name_str,
}
}
}
}
Fields::Unnamed(_) => syn::Error::new_spanned(
&input,
"injectable does not support tuple structs. Use named fields instead.",
)
.to_compile_error(),
}
}
_ => syn::Error::new_spanned(&input, "injectable can only be used on structs")
.to_compile_error(),
};
TokenStream::from(expanded)
}
fn generate_for_named_struct(
ferro: &proc_macro2::TokenStream,
name: &syn::Ident,
name_str: String,
vis: &syn::Visibility,
attrs: &[syn::Attribute],
generics: &syn::Generics,
fields_named: &FieldsNamed,
) -> proc_macro2::TokenStream {
let fields = &fields_named.named;
let has_injected_fields = fields.iter().any(has_inject_attr);
if has_injected_fields {
generate_with_injection(ferro, name, name_str, vis, attrs, generics, fields_named)
} else {
let fields_without_inject: Vec<_> = fields.iter().collect();
quote! {
#(#attrs)*
#[derive(Default, Clone)]
#vis struct #name #generics {
#(#fields_without_inject),*
}
#ferro::inventory::submit! {
#ferro::container::provider::SingletonEntry {
register: || {
#ferro::App::singleton(<#name as ::std::default::Default>::default());
},
name: #name_str,
}
}
}
}
}
fn generate_with_injection(
ferro: &proc_macro2::TokenStream,
name: &syn::Ident,
name_str: String,
vis: &syn::Visibility,
attrs: &[syn::Attribute],
generics: &syn::Generics,
fields_named: &FieldsNamed,
) -> proc_macro2::TokenStream {
let fields = &fields_named.named;
let mut field_definitions = Vec::new();
let mut field_initializations = Vec::new();
for field in fields {
let field_name = field.ident.as_ref().unwrap();
let field_ty = &field.ty;
let field_vis = &field.vis;
let other_attrs: Vec<_> = field
.attrs
.iter()
.filter(|attr| !attr.path().is_ident("inject"))
.collect();
field_definitions.push(quote! {
#(#other_attrs)*
#field_vis #field_name: #field_ty
});
if has_inject_attr(field) {
field_initializations.push(quote! {
#field_name: #ferro::App::resolve::<#field_ty>()
.expect(&format!(
"Failed to resolve dependency '{}' for '{}'. \
Make sure '{}' is registered before '{}'.",
stringify!(#field_ty),
#name_str,
stringify!(#field_ty),
#name_str
))
});
} else {
field_initializations.push(quote! {
#field_name: ::std::default::Default::default()
});
}
}
quote! {
#(#attrs)*
#[derive(Clone)]
#vis struct #name #generics {
#(#field_definitions),*
}
impl #name {
fn __resolve_dependencies() -> Self {
Self {
#(#field_initializations),*
}
}
}
#ferro::inventory::submit! {
#ferro::container::provider::SingletonEntry {
register: || {
#ferro::App::singleton(#name::__resolve_dependencies());
},
name: #name_str,
}
}
}
}