From 740ef47d49e02ae2f2184f4c347d8eba8aee38fd Mon Sep 17 00:00:00 2001 From: HampusM Date: Sat, 15 Oct 2022 18:40:20 +0200 Subject: refactor: improve internals of macros & add unit tests --- macros/Cargo.toml | 3 + macros/src/fn_trait.rs | 56 +- macros/src/injectable/dependency.rs | 337 ++++++++++-- macros/src/injectable/implementation.rs | 835 ++++++++++++++++++++++++------ macros/src/injectable/named_attr_input.rs | 17 +- macros/src/lib.rs | 14 +- macros/src/test_utils.rs | 99 ++++ src/lib.rs | 6 +- 8 files changed, 1142 insertions(+), 225 deletions(-) create mode 100644 macros/src/test_utils.rs diff --git a/macros/Cargo.toml b/macros/Cargo.toml index ff55afe..d71971e 100644 --- a/macros/Cargo.toml +++ b/macros/Cargo.toml @@ -26,6 +26,9 @@ proc-macro2 = "1.0.40" uuid = { version = "0.8", features = ["v4"] } regex = "1.6.0" once_cell = "1.13.1" +thiserror = "1.0.37" [dev_dependencies] syrette = { version = "0.4.0", path = "..", features = ["factory"] } +mockall = "0.11.1" +pretty_assertions = "1.3.0" diff --git a/macros/src/fn_trait.rs b/macros/src/fn_trait.rs index a52a00d..d88d391 100644 --- a/macros/src/fn_trait.rs +++ b/macros/src/fn_trait.rs @@ -92,22 +92,10 @@ mod tests use quote::{format_ident, quote}; use syn::token::{Dyn, RArrow}; - use syn::{parse2, Path, PathSegment, TypePath}; + use syn::{parse2, PathSegment}; use super::*; - - fn create_path(segments: &[PathSegment]) -> Path - { - Path { - leading_colon: None, - segments: segments.iter().cloned().collect(), - } - } - - fn create_type(path: Path) -> Type - { - Type::Path(TypePath { qself: None, path }) - } + use crate::test_utils; #[test] fn can_parse_fn_trait() -> Result<(), Box> @@ -121,15 +109,17 @@ mod tests trait_ident: format_ident!("Fn"), paren_token: Paren::default(), inputs: Punctuated::from_iter(vec![ - create_type(create_path(&[PathSegment::from(format_ident!( - "String" - ))])), - create_type(create_path(&[PathSegment::from(format_ident!("u32"))])) + test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("String")) + ])), + test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("u32")) + ])) ]), r_arrow_token: RArrow::default(), - output: create_type(create_path(&[PathSegment::from(format_ident!( - "Handle" - ))])), + output: test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Handle")) + ])), trait_bounds: Punctuated::new() } ); @@ -151,20 +141,20 @@ mod tests trait_ident: format_ident!("Fn"), paren_token: Paren::default(), inputs: Punctuated::from_iter(vec![ - create_type(create_path(&[PathSegment::from(format_ident!( - "Bread" - ))])), - create_type(create_path(&[PathSegment::from(format_ident!( - "Cheese" - ))])), - create_type(create_path(&[PathSegment::from(format_ident!( - "Tomatoes" - ))])) + test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Bread")) + ])), + test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Cheese")) + ])), + test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Tomatoes")) + ])) ]), r_arrow_token: RArrow::default(), - output: create_type(create_path(&[PathSegment::from(format_ident!( - "Taco" - ))])), + output: test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Taco")) + ])), trait_bounds: Punctuated::new() } .into_token_stream() diff --git a/macros/src/injectable/dependency.rs b/macros/src/injectable/dependency.rs index 2c5e0fd..314a369 100644 --- a/macros/src/injectable/dependency.rs +++ b/macros/src/injectable/dependency.rs @@ -1,56 +1,77 @@ -use std::error::Error; - use proc_macro2::Ident; use syn::{parse2, FnArg, GenericArgument, LitStr, PathArguments, Type}; use crate::injectable::named_attr_input::NamedAttrInput; use crate::util::syn_path::syn_path_to_string; +/// Interface for a representation of a dependency of a injectable type. +/// +/// Found as a argument in the 'new' method of the type. +#[cfg_attr(test, mockall::automock)] +pub trait IDependency: Sized +{ + /// Build a new `Dependency` from a argument in a 'new' method. + fn build(new_method_arg: &FnArg) -> Result; + + /// Returns the interface type. + fn get_interface(&self) -> &Type; + + /// Returns the pointer type identity. + fn get_ptr(&self) -> &Ident; + + /// Returns optional name of the dependency. + fn get_name(&self) -> &Option; +} + +/// Representation of a dependency of a injectable type. +/// +/// Found as a argument in the 'new' method of the type. +#[derive(Debug, PartialEq, Eq)] pub struct Dependency { - pub interface: Type, - pub ptr: Ident, - pub name: Option, + interface: Type, + ptr: Ident, + name: Option, } -impl Dependency +impl IDependency for Dependency { - pub fn build(new_method_arg: &FnArg) -> Result> + fn build(new_method_arg: &FnArg) -> Result { let typed_new_method_arg = match new_method_arg { FnArg::Typed(typed_arg) => Ok(typed_arg), - FnArg::Receiver(_) => Err("Unexpected self argument in 'new' method"), + FnArg::Receiver(_) => Err(DependencyError::UnexpectedSelfArgument), }?; - let ptr_type_path = match typed_new_method_arg.ty.as_ref() { + let dependency_type_path = match typed_new_method_arg.ty.as_ref() { Type::Path(arg_type_path) => Ok(arg_type_path), Type::Reference(ref_type_path) => match ref_type_path.elem.as_ref() { Type::Path(arg_type_path) => Ok(arg_type_path), - &_ => Err("Unexpected reference to non-path type"), + &_ => Err(DependencyError::TypeNotPath), }, - &_ => Err("Expected a path or a reference type"), + &_ => Err(DependencyError::TypeNotPath), }?; - let ptr_path_segment = ptr_type_path.path.segments.last().map_or_else( - || Err("Expected pointer type path to have a last segment"), - Ok, - )?; + let ptr_path_segment = dependency_type_path + .path + .segments + .last() + .map_or_else(|| Err(DependencyError::PtrTypePathEmpty), Ok)?; - let ptr = ptr_path_segment.ident.clone(); + let ptr_ident = ptr_path_segment.ident.clone(); - let ptr_path_generic_args = &match &ptr_path_segment.arguments { + let ptr_generic_args = &match &ptr_path_segment.arguments { PathArguments::AngleBracketed(generic_args) => Ok(generic_args), - &_ => Err("Expected pointer type to have a generic type argument"), + &_ => Err(DependencyError::PtrTypeNoGenerics), }? .args; - let interface = if let Some(GenericArgument::Type(interface)) = - ptr_path_generic_args.first() - { - Ok(interface.clone()) - } else { - Err("Expected pointer type to have a generic type argument") - }?; + let interface = + if let Some(GenericArgument::Type(interface)) = ptr_generic_args.first() { + Ok(interface.clone()) + } else { + Err(DependencyError::PtrTypeNoGenerics) + }?; let arg_attrs = &typed_new_method_arg.attrs; @@ -63,19 +84,267 @@ impl Dependency let opt_named_attr_tokens = opt_named_attr.map(|attr| &attr.tokens); - let opt_named_attr_input = - if let Some(named_attr_tokens) = opt_named_attr_tokens { - Some(parse2::(named_attr_tokens.clone()).map_err( - |err| format!("Invalid input for 'named' attribute. {}", err), - )?) - } else { - None - }; + let opt_named_attr_input = if let Some(named_attr_tokens) = opt_named_attr_tokens + { + Some( + parse2::(named_attr_tokens.clone()) + .map_err(DependencyError::ParseNamedAttrInputFailed)?, + ) + } else { + None + }; Ok(Self { interface, - ptr, + ptr: ptr_ident, name: opt_named_attr_input.map(|named_attr_input| named_attr_input.name), }) } + + fn get_interface(&self) -> &Type + { + &self.interface + } + + fn get_ptr(&self) -> &Ident + { + &self.ptr + } + + fn get_name(&self) -> &Option + { + &self.name + } +} + +#[derive(Debug, thiserror::Error)] +pub enum DependencyError +{ + #[error("Unexpected 'self' argument in 'new' method")] + UnexpectedSelfArgument, + + #[error("Argument type must either be a path or a path reference")] + TypeNotPath, + + #[error("Expected pointer type path to not be empty")] + PtrTypePathEmpty, + + #[error("Expected pointer type to take generic arguments")] + PtrTypeNoGenerics, + + #[error("Failed to parse the input to a 'named' attribute")] + ParseNamedAttrInputFailed(#[source] syn::Error), +} + +#[cfg(test)] +mod tests +{ + use std::error::Error; + + use proc_macro::TokenStream; + use proc_macro2::Span; + use quote::{format_ident, quote}; + use syn::punctuated::Punctuated; + use syn::token::{And, Bang, Bracket, Colon, Paren, Pound, SelfValue}; + use syn::{ + AttrStyle, + Attribute, + Pat, + PatType, + PathSegment, + Receiver, + TypeNever, + TypeReference, + TypeTuple, + }; + + use super::*; + use crate::test_utils; + + #[test] + fn can_build_dependency() -> Result<(), Box> + { + assert_eq!( + Dependency::build(&FnArg::Typed(PatType { + attrs: vec![], + pat: Box::new(Pat::Verbatim(TokenStream::default().into())), + colon_token: Colon::default(), + ty: Box::new(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("TransientPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Foo"), &[]) + ]))] + ), + ]))) + }))?, + Dependency { + interface: test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Foo")) + ])), + ptr: format_ident!("TransientPtr"), + name: None + } + ); + + assert_eq!( + Dependency::build(&FnArg::Typed(PatType { + attrs: vec![], + pat: Box::new(Pat::Verbatim(TokenStream::default().into())), + colon_token: Colon::default(), + ty: Box::new(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("syrette"), &[]), + test_utils::create_path_segment(format_ident!("ptr"), &[]), + test_utils::create_path_segment( + format_ident!("SingletonPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Bar"), &[]) + ]))] + ), + ]))) + }))?, + Dependency { + interface: test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Bar")) + ])), + ptr: format_ident!("SingletonPtr"), + name: None + } + ); + + Ok(()) + } + + #[test] + fn can_build_dependency_with_name() -> Result<(), Box> + { + assert_eq!( + Dependency::build(&FnArg::Typed(PatType { + attrs: vec![Attribute { + pound_token: Pound::default(), + style: AttrStyle::Outer, + bracket_token: Bracket::default(), + path: test_utils::create_path(&[test_utils::create_path_segment( + format_ident!("named"), + &[] + )]), + tokens: quote! { ("cool") } + }], + pat: Box::new(Pat::Verbatim(TokenStream::default().into())), + colon_token: Colon::default(), + ty: Box::new(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("TransientPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Foo"), &[]) + ]))] + ), + ]))) + }))?, + Dependency { + interface: test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Foo")) + ])), + ptr: format_ident!("TransientPtr"), + name: Some(LitStr::new("cool", Span::call_site())) + } + ); + + assert_eq!( + Dependency::build(&FnArg::Typed(PatType { + attrs: vec![Attribute { + pound_token: Pound::default(), + style: AttrStyle::Outer, + bracket_token: Bracket::default(), + path: test_utils::create_path(&[test_utils::create_path_segment( + format_ident!("named"), + &[] + )]), + tokens: quote! { ("awesome") } + }], + pat: Box::new(Pat::Verbatim(TokenStream::default().into())), + colon_token: Colon::default(), + ty: Box::new(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("syrette"), &[]), + test_utils::create_path_segment(format_ident!("ptr"), &[]), + test_utils::create_path_segment( + format_ident!("FactoryPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Bar"), &[]) + ]))] + ), + ]))) + }))?, + Dependency { + interface: test_utils::create_type(test_utils::create_path(&[ + PathSegment::from(format_ident!("Bar")) + ])), + ptr: format_ident!("FactoryPtr"), + name: Some(LitStr::new("awesome", Span::call_site())) + } + ); + + Ok(()) + } + + #[test] + fn cannot_build_dependency_with_receiver_arg() + { + assert!(Dependency::build(&FnArg::Receiver(Receiver { + attrs: vec![], + reference: None, + mutability: None, + self_token: SelfValue::default() + })) + .is_err()); + } + + #[test] + fn cannot_build_dependency_with_type_not_path() + { + assert!(Dependency::build(&FnArg::Typed(PatType { + attrs: vec![], + pat: Box::new(Pat::Verbatim(TokenStream::default().into())), + colon_token: Colon::default(), + ty: Box::new(Type::Tuple(TypeTuple { + paren_token: Paren::default(), + elems: Punctuated::from_iter(vec![test_utils::create_type( + test_utils::create_path(&[test_utils::create_path_segment( + format_ident!("EvilType"), + &[] + )]) + )]) + })) + })) + .is_err()); + + assert!(Dependency::build(&FnArg::Typed(PatType { + attrs: vec![], + pat: Box::new(Pat::Verbatim(TokenStream::default().into())), + colon_token: Colon::default(), + ty: Box::new(Type::Reference(TypeReference { + and_token: And::default(), + lifetime: None, + mutability: None, + elem: Box::new(Type::Never(TypeNever { + bang_token: Bang::default() + })) + })) + })) + .is_err()); + } + + #[test] + fn cannot_build_dependency_without_generics_args() + { + assert!(Dependency::build(&FnArg::Typed(PatType { + attrs: vec![], + pat: Box::new(Pat::Verbatim(TokenStream::default().into())), + colon_token: Colon::default(), + ty: Box::new(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("TransientPtr"), &[]), + ]))) + })) + .is_err()); + } } diff --git a/macros/src/injectable/implementation.rs b/macros/src/injectable/implementation.rs index c907f32..9542a98 100644 --- a/macros/src/injectable/implementation.rs +++ b/macros/src/injectable/implementation.rs @@ -1,10 +1,11 @@ use std::error::Error; +use proc_macro2::Ident; use quote::{format_ident, quote, ToTokens}; use syn::parse::{Parse, ParseStream}; -use syn::{parse_str, ExprMethodCall, FnArg, Generics, ItemImpl, Type}; +use syn::{parse_str, ExprMethodCall, FnArg, Generics, ImplItemMethod, ItemImpl, Type}; -use crate::injectable::dependency::Dependency; +use crate::injectable::dependency::IDependency; use crate::util::item_impl::find_impl_method_by_name_mut; use crate::util::string::camelcase_to_snakecase; use crate::util::syn_path::syn_path_to_string; @@ -12,131 +13,144 @@ use crate::util::syn_path::syn_path_to_string; const DI_CONTAINER_VAR_NAME: &str = "di_container"; const DEPENDENCY_HISTORY_VAR_NAME: &str = "dependency_history"; -pub struct InjectableImpl +pub struct InjectableImpl { - pub dependencies: Vec, + pub dependencies: Vec, pub self_type: Type, pub generics: Generics, pub original_impl: ItemImpl, } -impl Parse for InjectableImpl +impl Parse for InjectableImpl { + #[cfg(not(tarpaulin_include))] fn parse(input: ParseStream) -> syn::Result { - let mut impl_parsed_input = input.parse::()?; + let input_fork = input.fork(); - let dependencies = Self::build_dependencies(&mut impl_parsed_input) - .map_err(|err| input.error(err))?; + let mut item_impl = input.parse::()?; + + let new_method = find_impl_method_by_name_mut(&mut item_impl, "new") + .map_or_else(|| Err(input_fork.error("Missing a 'new' method")), Ok)?; + + let dependencies = + Self::build_dependencies(new_method).map_err(|err| input.error(err))?; + + Self::remove_method_argument_attrs(new_method); Ok(Self { dependencies, - self_type: impl_parsed_input.self_ty.as_ref().clone(), - generics: impl_parsed_input.generics.clone(), - original_impl: impl_parsed_input, + self_type: item_impl.self_ty.as_ref().clone(), + generics: item_impl.generics.clone(), + original_impl: item_impl, }) } } -impl InjectableImpl +impl InjectableImpl { + #[cfg(not(tarpaulin_include))] pub fn expand(&self, no_doc_hidden: bool, is_async: bool) -> proc_macro2::TokenStream { - let Self { - dependencies, - self_type, - generics, - original_impl, - } = self; - let di_container_var = format_ident!("{}", DI_CONTAINER_VAR_NAME); let dependency_history_var = format_ident!("{}", DEPENDENCY_HISTORY_VAR_NAME); let maybe_doc_hidden = if no_doc_hidden { quote! {} } else { - quote! { - #[doc(hidden)] - } + quote! { #[doc(hidden)] } }; let maybe_prevent_circular_deps = if cfg!(feature = "prevent-circular") { - quote! { - if #dependency_history_var.contains(&self_type_name) { - #dependency_history_var.push(self_type_name); - - let dependency_trace = - syrette::dependency_trace::create_dependency_trace( - #dependency_history_var.as_slice(), - self_type_name - ); - - return Err(InjectableError::DetectedCircular {dependency_trace }); - } - - #dependency_history_var.push(self_type_name); - } + Self::expand_prevent_circular_deps(&dependency_history_var) } else { quote! {} }; let injectable_impl = if is_async { - let async_get_dep_method_calls = - Self::create_get_dep_method_calls(dependencies, true); + self.expand_async_impl( + &maybe_doc_hidden, + &di_container_var, + &dependency_history_var, + &maybe_prevent_circular_deps, + &Self::create_get_dep_method_calls(&self.dependencies, true).unwrap(), + ) + } else { + self.expand_blocking_impl( + &maybe_doc_hidden, + &di_container_var, + &dependency_history_var, + &maybe_prevent_circular_deps, + &Self::create_get_dep_method_calls(&self.dependencies, false).unwrap(), + ) + }; - quote! { - #maybe_doc_hidden - impl #generics syrette::interfaces::async_injectable::AsyncInjectable for #self_type - { - fn resolve<'di_container, 'fut>( - #di_container_var: &'di_container std::sync::Arc< - syrette::AsyncDIContainer - >, - mut #dependency_history_var: Vec<&'static str>, - ) -> syrette::future::BoxFuture< - 'fut, - Result< - syrette::ptr::TransientPtr, - syrette::errors::injectable::InjectableError - > - > - where - Self: Sized + 'fut, - 'di_container: 'fut - { - Box::pin(async move { - use std::any::type_name; + let original_impl = &self.original_impl; - use syrette::errors::injectable::InjectableError; + quote! { + #original_impl - let self_type_name = type_name::<#self_type>(); + #injectable_impl + } + } - #maybe_prevent_circular_deps + #[cfg(not(tarpaulin_include))] + fn expand_prevent_circular_deps( + dependency_history_var: &Ident, + ) -> proc_macro2::TokenStream + { + quote! { + if #dependency_history_var.contains(&self_type_name) { + #dependency_history_var.push(self_type_name); - Ok(syrette::ptr::TransientPtr::new(Self::new( - #(#async_get_dep_method_calls),* - ))) - }) - } - } + let dependency_trace = + syrette::dependency_trace::create_dependency_trace( + #dependency_history_var.as_slice(), + self_type_name + ); + return Err(InjectableError::DetectedCircular {dependency_trace }); } - } else { - let get_dep_method_calls = - Self::create_get_dep_method_calls(dependencies, false); - quote! { - #maybe_doc_hidden - impl #generics syrette::interfaces::injectable::Injectable for #self_type - { - fn resolve( - #di_container_var: &std::rc::Rc, - mut #dependency_history_var: Vec<&'static str>, - ) -> Result< + #dependency_history_var.push(self_type_name); + } + } + + #[cfg(not(tarpaulin_include))] + fn expand_async_impl( + &self, + maybe_doc_hidden: &proc_macro2::TokenStream, + di_container_var: &Ident, + dependency_history_var: &Ident, + maybe_prevent_circular_deps: &proc_macro2::TokenStream, + get_dep_method_calls: &Vec, + ) -> proc_macro2::TokenStream + { + let generics = &self.generics; + let self_type = &self.self_type; + + quote! { + #maybe_doc_hidden + impl #generics syrette::interfaces::async_injectable::AsyncInjectable for #self_type + { + fn resolve<'di_container, 'fut>( + #di_container_var: &'di_container std::sync::Arc< + syrette::AsyncDIContainer + >, + mut #dependency_history_var: Vec<&'static str>, + ) -> syrette::future::BoxFuture< + 'fut, + Result< syrette::ptr::TransientPtr, - syrette::errors::injectable::InjectableError> - { + syrette::errors::injectable::InjectableError + > + > + where + Self: Sized + 'fut, + 'di_container: 'fut + { + Box::pin(async move { use std::any::type_name; use syrette::errors::injectable::InjectableError; @@ -145,99 +159,147 @@ impl InjectableImpl #maybe_prevent_circular_deps - return Ok(syrette::ptr::TransientPtr::new(Self::new( + Ok(syrette::ptr::TransientPtr::new(Self::new( #(#get_dep_method_calls),* - ))); - } + ))) + }) } } - }; + } + } + + #[cfg(not(tarpaulin_include))] + fn expand_blocking_impl( + &self, + maybe_doc_hidden: &proc_macro2::TokenStream, + di_container_var: &Ident, + dependency_history_var: &Ident, + maybe_prevent_circular_deps: &proc_macro2::TokenStream, + get_dep_method_calls: &Vec, + ) -> proc_macro2::TokenStream + { + let generics = &self.generics; + let self_type = &self.self_type; quote! { - #original_impl + #maybe_doc_hidden + impl #generics syrette::interfaces::injectable::Injectable for #self_type + { + fn resolve( + #di_container_var: &std::rc::Rc, + mut #dependency_history_var: Vec<&'static str>, + ) -> Result< + syrette::ptr::TransientPtr, + syrette::errors::injectable::InjectableError> + { + use std::any::type_name; - #injectable_impl + use syrette::errors::injectable::InjectableError; + + let self_type_name = type_name::<#self_type>(); + + #maybe_prevent_circular_deps + + return Ok(syrette::ptr::TransientPtr::new(Self::new( + #(#get_dep_method_calls),* + ))); + } + } } } fn create_get_dep_method_calls( - dependencies: &[Dependency], + dependencies: &[Dep], is_async: bool, - ) -> Vec + ) -> Result, Box> { dependencies .iter() .filter_map(|dependency| { - let dep_interface_str = match &dependency.interface { - Type::TraitObject(interface_trait) => { - Some(interface_trait.to_token_stream().to_string()) - } - Type::Path(path_interface) => { - Some(syn_path_to_string(&path_interface.path)) - } - &_ => None, + match dependency.get_interface() { + Type::TraitObject(_) | Type::Path(_) => Some(()), + _ => None, }?; - let method_call = parse_str::( - format!( - "{}.get_bound::<{}>({}.clone(), {})", - DI_CONTAINER_VAR_NAME, - dep_interface_str, - DEPENDENCY_HISTORY_VAR_NAME, - dependency.name.as_ref().map_or_else( - || "None".to_string(), - |name| format!("Some(\"{}\")", name.value()) - ) - ) - .as_str(), - ) - .ok()?; - - Some((method_call, dependency)) - }) - .map(|(method_call, dep_type)| { - let ptr_name = dep_type.ptr.to_string(); - - let to_ptr = format_ident!( - "{}", - camelcase_to_snakecase(&ptr_name.replace("Ptr", "")) - ); - - let do_method_call = if is_async { - quote! { #method_call.await } - } else { - quote! { #method_call } - }; - - let resolve_failed_error = if is_async { - quote! { InjectableError::AsyncResolveFailed } - } else { - quote! { InjectableError::ResolveFailed } - }; - - quote! { - #do_method_call.map_err(|err| #resolve_failed_error { - reason: Box::new(err), - affected: self_type_name - })?.#to_ptr().unwrap() - } + Some(Self::create_single_get_dep_method_call( + dependency, is_async, + )) }) .collect() } - fn build_dependencies( - item_impl: &mut ItemImpl, - ) -> Result, Box> + fn create_single_get_dep_method_call( + dependency: &Dep, + is_async: bool, + ) -> Result> + { + let dep_interface_str = match &dependency.get_interface() { + Type::TraitObject(interface_trait) => { + Ok(interface_trait.to_token_stream().to_string()) + } + Type::Path(path_interface) => Ok(syn_path_to_string(&path_interface.path)), + &_ => Err("Invalid type. Expected trait type or path type"), + }?; + + let method_call = parse_str::( + format!( + "{}.get_bound::<{}>({}.clone(), {})", + DI_CONTAINER_VAR_NAME, + dep_interface_str, + DEPENDENCY_HISTORY_VAR_NAME, + dependency.get_name().as_ref().map_or_else( + || "None".to_string(), + |name| format!("Some(\"{}\")", name.value()) + ) + ) + .as_str(), + )?; + + let ptr_name = dependency.get_ptr().to_string(); + + let to_ptr = + format_ident!("{}", camelcase_to_snakecase(&ptr_name.replace("Ptr", ""))); + + let do_method_call = if is_async { + quote! { #method_call.await } + } else { + quote! { #method_call } + }; + + let resolve_failed_error = if is_async { + quote! { InjectableError::AsyncResolveFailed } + } else { + quote! { InjectableError::ResolveFailed } + }; + + Ok(quote! { + #do_method_call.map_err(|err| #resolve_failed_error { + reason: Box::new(err), + affected: self_type_name + })?.#to_ptr().unwrap() + }) + } + + fn build_dependencies(new_method: &ImplItemMethod) + -> Result, Box> { - let new_method_impl_item = find_impl_method_by_name_mut(item_impl, "new") - .map_or_else(|| Err("Missing a 'new' method"), Ok)?; + let new_method_args = &new_method.sig.inputs; - let new_method_args = &mut new_method_impl_item.sig.inputs; + let dependencies_result: Result, _> = + new_method_args.iter().map(Dep::build).collect(); - let dependencies: Result, _> = - new_method_args.iter().map(Dependency::build).collect(); + let deps = dependencies_result?; - for arg in new_method_args { + Ok(deps) + } + + // Removes argument attributes from a method, as they are not actually valid Rust. + // Not doing this would cause a compilation error. + fn remove_method_argument_attrs(method: &mut ImplItemMethod) + { + let method_args = &mut method.sig.inputs; + + for arg in method_args { let typed_arg = if let FnArg::Typed(typed_arg) = arg { typed_arg } else { @@ -265,7 +327,476 @@ impl InjectableImpl typed_arg.attrs.remove(attr_index); } } + } +} + +#[cfg(test)] +mod tests +{ + use std::sync::{Mutex, MutexGuard}; + + use once_cell::sync::Lazy; + use pretty_assertions::assert_eq; + use proc_macro2::{Span, TokenStream}; + use syn::token::{Brace, Bracket, Colon, Paren, Pound}; + use syn::{ + parse2, + AttrStyle, + Attribute, + Block, + ImplItemMethod, + LitStr, + Pat, + PatType, + Visibility, + }; + + use super::*; + use crate::injectable::dependency::MockIDependency; + use crate::injectable::named_attr_input::NamedAttrInput; + use crate::test_utils; + + static TEST_MUTEX: Lazy> = Lazy::new(|| Mutex::new(())); + + // When a test panics, it will poison the Mutex. Since we don't actually + // care about the state of the data we ignore that it is poisoned and grab + // the lock regardless. If you just do `let _lock = &TEST_MUTEX.lock().unwrap()`, one + // test panicking will cause all other tests that try and acquire a lock on + // that Mutex to also panic. + fn get_lock(m: &'static Mutex<()>) -> MutexGuard<'static, ()> + { + match m.lock() { + Ok(guard) => guard, + Err(poisoned) => poisoned.into_inner(), + } + } - dependencies + #[test] + fn can_build_dependencies() -> Result<(), Box> + { + let method = ImplItemMethod { + attrs: vec![], + vis: Visibility::Inherited, + defaultness: None, + sig: test_utils::create_signature( + format_ident!("new"), + vec![ + ( + test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("TransientPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("Foo"), + &[], + ), + ]))], + ), + ])), + vec![], + ), + ( + test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("FactoryPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("BarFactory"), + &[], + ), + ]))], + ), + ])), + vec![], + ), + ], + test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Self"), &[]), + ])), + ), + block: Block { + brace_token: Brace::default(), + stmts: vec![], + }, + }; + + let _lock = get_lock(&TEST_MUTEX); + + let build_context = MockIDependency::build_context(); + + build_context + .expect() + .returning(|_| Ok(MockIDependency::new())); + + let dependencies = + InjectableImpl::::build_dependencies(&method)?; + + assert_eq!(dependencies.len(), 2); + + Ok(()) + } + + #[test] + fn can_build_named_dependencies() -> Result<(), Box> + { + let method = ImplItemMethod { + attrs: vec![], + vis: Visibility::Inherited, + defaultness: None, + sig: test_utils::create_signature( + format_ident!("new"), + vec![ + ( + test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("TransientPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("Foo"), + &[], + ), + ]))], + ), + ])), + vec![], + ), + ( + test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("FactoryPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("BarFactory"), + &[], + ), + ]))], + ), + ])), + vec![Attribute { + pound_token: Pound::default(), + style: AttrStyle::Outer, + bracket_token: Bracket::default(), + path: test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("named"), + &[], + ), + ]), + tokens: NamedAttrInput { + paren: Paren::default(), + name: LitStr::new("awesome", Span::call_site()), + } + .to_token_stream(), + }], + ), + ], + test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Self"), &[]), + ])), + ), + block: Block { + brace_token: Brace::default(), + stmts: vec![], + }, + }; + + let _lock = get_lock(&TEST_MUTEX); + + let build_context = MockIDependency::build_context(); + + build_context + .expect() + .returning(|_| Ok(MockIDependency::new())) + .times(2); + + let dependencies = + InjectableImpl::::build_dependencies(&method)?; + + assert_eq!(dependencies.len(), 2); + + Ok(()) + } + + #[test] + fn can_remove_method_argument_attrs() + { + let first_arg_type = test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("TransientPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Foo"), &[]), + ]))], + ), + ])); + + let second_arg_type = test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("FactoryPtr"), + &[test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("BarFactory"), &[]), + ]))], + ), + ])); + + let mut method = ImplItemMethod { + attrs: vec![], + vis: Visibility::Inherited, + defaultness: None, + sig: test_utils::create_signature( + format_ident!("new"), + vec![ + ( + first_arg_type.clone(), + vec![Attribute { + pound_token: Pound::default(), + style: AttrStyle::Outer, + bracket_token: Bracket::default(), + path: test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("named"), + &[], + ), + ]), + tokens: NamedAttrInput { + paren: Paren::default(), + name: LitStr::new("cool", Span::call_site()), + } + .to_token_stream(), + }], + ), + ( + second_arg_type.clone(), + vec![Attribute { + pound_token: Pound::default(), + style: AttrStyle::Outer, + bracket_token: Bracket::default(), + path: test_utils::create_path(&[ + test_utils::create_path_segment( + format_ident!("named"), + &[], + ), + ]), + tokens: NamedAttrInput { + paren: Paren::default(), + name: LitStr::new("awesome", Span::call_site()), + } + .to_token_stream(), + }], + ), + ], + test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Self"), &[]), + ])), + ), + block: Block { + brace_token: Brace::default(), + stmts: vec![], + }, + }; + + InjectableImpl::::remove_method_argument_attrs(&mut method); + + assert_eq!( + method.sig.inputs.first().unwrap().clone(), + FnArg::Typed(PatType { + attrs: vec![], + pat: Box::new(Pat::Verbatim(TokenStream::new())), + colon_token: Colon::default(), + ty: Box::new(first_arg_type), + }) + ); + + assert_eq!( + method.sig.inputs.last().unwrap().clone(), + FnArg::Typed(PatType { + attrs: vec![], + pat: Box::new(Pat::Verbatim(TokenStream::new())), + colon_token: Colon::default(), + ty: Box::new(second_arg_type), + }) + ); + } + + #[test] + fn can_create_single_get_dep_method_call() -> Result<(), Box> + { + let mut mock_dependency = MockIDependency::new(); + + mock_dependency + .expect_get_interface() + .return_const(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Foo"), &[]), + ]))); + + mock_dependency.expect_get_name().return_const(None); + + mock_dependency + .expect_get_ptr() + .return_const(format_ident!("TransientPtr")); + + let di_container_var_ident = format_ident!("{}", DI_CONTAINER_VAR_NAME); + let dep_history_var_ident = format_ident!("{}", DEPENDENCY_HISTORY_VAR_NAME); + + let output = + InjectableImpl::::create_single_get_dep_method_call( + &mock_dependency, + false, + )?; + + assert_eq!( + parse2::(output)?, + parse2::(quote! { + #di_container_var_ident + .get_bound::(#dep_history_var_ident.clone(), None) + .map_err(|err| InjectableError::ResolveFailed { + reason: Box::new(err), + affected: self_type_name + })? + .transient() + .unwrap() + + })? + ); + + Ok(()) + } + + #[test] + fn can_create_single_get_dep_method_call_with_name() -> Result<(), Box> + { + let mut mock_dependency = MockIDependency::new(); + + mock_dependency + .expect_get_interface() + .return_const(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Foo"), &[]), + ]))); + + mock_dependency + .expect_get_name() + .return_const(Some(LitStr::new("special", Span::call_site()))); + + mock_dependency + .expect_get_ptr() + .return_const(format_ident!("TransientPtr")); + + let di_container_var_ident = format_ident!("{}", DI_CONTAINER_VAR_NAME); + let dep_history_var_ident = format_ident!("{}", DEPENDENCY_HISTORY_VAR_NAME); + + let output = + InjectableImpl::::create_single_get_dep_method_call( + &mock_dependency, + false, + )?; + + assert_eq!( + parse2::(output)?, + parse2::(quote! { + #di_container_var_ident + .get_bound::(#dep_history_var_ident.clone(), Some("special")) + .map_err(|err| InjectableError::ResolveFailed { + reason: Box::new(err), + affected: self_type_name + })? + .transient() + .unwrap() + + })? + ); + + Ok(()) + } + + #[test] + fn can_create_single_get_dep_method_call_async() -> Result<(), Box> + { + let mut mock_dependency = MockIDependency::new(); + + mock_dependency + .expect_get_interface() + .return_const(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Foo"), &[]), + ]))); + + mock_dependency.expect_get_name().return_const(None); + + mock_dependency + .expect_get_ptr() + .return_const(format_ident!("TransientPtr")); + + let di_container_var_ident = format_ident!("{}", DI_CONTAINER_VAR_NAME); + let dep_history_var_ident = format_ident!("{}", DEPENDENCY_HISTORY_VAR_NAME); + + let output = + InjectableImpl::::create_single_get_dep_method_call( + &mock_dependency, + true, + )?; + + assert_eq!( + parse2::(output)?, + parse2::(quote! { + #di_container_var_ident + .get_bound::(#dep_history_var_ident.clone(), None) + .await + .map_err(|err| InjectableError::AsyncResolveFailed { + reason: Box::new(err), + affected: self_type_name + })? + .transient() + .unwrap() + + })? + ); + + Ok(()) + } + + #[test] + fn can_create_single_get_dep_method_call_async_with_name( + ) -> Result<(), Box> + { + let mut mock_dependency = MockIDependency::new(); + + mock_dependency + .expect_get_interface() + .return_const(test_utils::create_type(test_utils::create_path(&[ + test_utils::create_path_segment(format_ident!("Foo"), &[]), + ]))); + + mock_dependency + .expect_get_name() + .return_const(Some(LitStr::new("foobar", Span::call_site()))); + + mock_dependency + .expect_get_ptr() + .return_const(format_ident!("TransientPtr")); + + let di_container_var_ident = format_ident!("{}", DI_CONTAINER_VAR_NAME); + let dep_history_var_ident = format_ident!("{}", DEPENDENCY_HISTORY_VAR_NAME); + + let output = + InjectableImpl::::create_single_get_dep_method_call( + &mock_dependency, + true, + )?; + + assert_eq!( + parse2::(output)?, + parse2::(quote! { + #di_container_var_ident + .get_bound::(#dep_history_var_ident.clone(), Some("foobar")) + .await + .map_err(|err| InjectableError::AsyncResolveFailed { + reason: Box::new(err), + affected: self_type_name + })? + .transient() + .unwrap() + + })? + ); + + Ok(()) } } diff --git a/macros/src/injectable/named_attr_input.rs b/macros/src/injectable/named_attr_input.rs index 5f7123c..eaa9dbf 100644 --- a/macros/src/injectable/named_attr_input.rs +++ b/macros/src/injectable/named_attr_input.rs @@ -1,8 +1,11 @@ +use quote::ToTokens; use syn::parse::Parse; +use syn::token::Paren; use syn::{parenthesized, LitStr}; pub struct NamedAttrInput { + pub paren: Paren, pub name: LitStr, } @@ -12,10 +15,22 @@ impl Parse for NamedAttrInput { let content; - parenthesized!(content in input); + let paren = parenthesized!(content in input); Ok(Self { + paren, name: content.parse()?, }) } } + +impl ToTokens for NamedAttrInput +{ + fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) + { + self.paren + .surround(&mut self.name.to_token_stream(), |stream| { + stream.to_tokens(tokens); + }); + } +} diff --git a/macros/src/lib.rs b/macros/src/lib.rs index 293ac06..298a717 100644 --- a/macros/src/lib.rs +++ b/macros/src/lib.rs @@ -1,4 +1,5 @@ #![cfg_attr(doc_cfg, feature(doc_cfg))] +#![cfg_attr(test, feature(is_some_with))] #![deny(clippy::all)] #![deny(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] @@ -24,7 +25,11 @@ mod factory; #[cfg_attr(doc_cfg, doc(cfg(feature = "factory")))] mod fn_trait; +#[cfg(test)] +mod test_utils; + use crate::declare_interface_args::DeclareInterfaceArgs; +use crate::injectable::dependency::Dependency; use crate::injectable::implementation::InjectableImpl; use crate::injectable::macro_args::InjectableMacroArgs; use crate::libs::intertrait_macros::gen_caster::generate_caster; @@ -112,6 +117,7 @@ use crate::libs::intertrait_macros::gen_caster::generate_caster; /// [`AsyncDIContainer`]: https://docs.rs/syrette/latest/syrette/async_di_container/struct.AsyncDIContainer.html /// [`Injectable`]: https://docs.rs/syrette/latest/syrette/interfaces/injectable/trait.Injectable.html /// [`di_container_bind`]: https://docs.rs/syrette/latest/syrette/macro.di_container_bind.html +#[cfg(not(tarpaulin_include))] #[proc_macro_attribute] pub fn injectable(args_stream: TokenStream, impl_stream: TokenStream) -> TokenStream { @@ -127,7 +133,7 @@ pub fn injectable(args_stream: TokenStream, impl_stream: TokenStream) -> TokenSt .find(|flag| flag.flag.to_string().as_str() == "async") .map_or(false, |flag| flag.is_on.value); - let injectable_impl: InjectableImpl = parse(impl_stream).unwrap(); + let injectable_impl: InjectableImpl = parse(impl_stream).unwrap(); let expanded_injectable_impl = injectable_impl.expand(no_doc_hidden, is_async); @@ -195,6 +201,7 @@ pub fn injectable(args_stream: TokenStream, impl_stream: TokenStream) -> TokenSt /// [`TransientPtr`]: https://docs.rs/syrette/latest/syrette/ptr/type.TransientPtr.html #[cfg(feature = "factory")] #[cfg_attr(doc_cfg, doc(cfg(feature = "factory")))] +#[cfg(not(tarpaulin_include))] #[proc_macro_attribute] pub fn factory(args_stream: TokenStream, type_alias_stream: TokenStream) -> TokenStream { @@ -291,9 +298,10 @@ pub fn factory(args_stream: TokenStream, type_alias_stream: TokenStream) -> Toke /// /// declare_default_factory!(dyn IParser); /// ``` -#[proc_macro] #[cfg(feature = "factory")] #[cfg_attr(doc_cfg, doc(cfg(feature = "factory")))] +#[cfg(not(tarpaulin_include))] +#[proc_macro] pub fn declare_default_factory(args_stream: TokenStream) -> TokenStream { use syn::parse_str; @@ -364,6 +372,7 @@ pub fn declare_default_factory(args_stream: TokenStream) -> TokenStream /// # /// declare_interface!(Ninja -> INinja); /// ``` +#[cfg(not(tarpaulin_include))] #[proc_macro] pub fn declare_interface(input: TokenStream) -> TokenStream { @@ -423,6 +432,7 @@ pub fn declare_interface(input: TokenStream) -> TokenStream /// # /// # impl INinja for Ninja {} /// ``` +#[cfg(not(tarpaulin_include))] #[proc_macro_attribute] pub fn named(_: TokenStream, _: TokenStream) -> TokenStream { diff --git a/macros/src/test_utils.rs b/macros/src/test_utils.rs new file mode 100644 index 0000000..471dc74 --- /dev/null +++ b/macros/src/test_utils.rs @@ -0,0 +1,99 @@ +use proc_macro2::{Ident, TokenStream}; +use syn::punctuated::Punctuated; +use syn::token::{Colon, Gt, Lt, Paren, RArrow}; +use syn::{ + AngleBracketedGenericArguments, + Attribute, + FnArg, + GenericArgument, + GenericParam, + Generics, + Pat, + PatType, + Path, + PathArguments, + PathSegment, + Signature, + Type, + TypePath, +}; + +pub fn create_path(segments: &[PathSegment]) -> Path +{ + Path { + leading_colon: None, + segments: segments.iter().cloned().collect(), + } +} + +pub fn create_path_segment(ident: Ident, generic_arg_types: &[Type]) -> PathSegment +{ + PathSegment { + ident, + arguments: if generic_arg_types.is_empty() { + PathArguments::None + } else { + PathArguments::AngleBracketed(AngleBracketedGenericArguments { + colon2_token: None, + lt_token: Lt::default(), + args: generic_arg_types + .iter() + .map(|generic_arg_type| { + GenericArgument::Type(generic_arg_type.clone()) + }) + .collect(), + gt_token: Gt::default(), + }) + }, + } +} + +pub fn create_type(path: Path) -> Type +{ + Type::Path(TypePath { qself: None, path }) +} + +pub fn create_generics(params: Params) -> Generics +where + Params: IntoIterator, +{ + Generics { + lt_token: None, + params: Punctuated::from_iter(params), + gt_token: None, + where_clause: None, + } +} + +pub fn create_signature( + ident: Ident, + arg_types: ArgTypes, + return_type: Type, +) -> Signature +where + ArgTypes: IntoIterator)>, +{ + Signature { + constness: None, + asyncness: None, + unsafety: None, + abi: None, + fn_token: syn::token::Fn::default(), + ident, + generics: create_generics(vec![]), + paren_token: Paren::default(), + inputs: arg_types + .into_iter() + .map(|(arg_type, attrs)| { + FnArg::Typed(PatType { + attrs, + pat: Box::new(Pat::Verbatim(TokenStream::new())), + colon_token: Colon::default(), + ty: Box::new(arg_type), + }) + }) + .collect(), + variadic: None, + output: syn::ReturnType::Type(RArrow::default(), Box::new(return_type)), + } +} diff --git a/src/lib.rs b/src/lib.rs index 2cc3cc4..a3da74a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,7 +1,5 @@ #![cfg_attr(feature = "factory", feature(unboxed_closures, fn_traits))] #![cfg_attr(doc_cfg, feature(doc_cfg))] -#![cfg_attr(test, feature(register_tool))] -#![cfg_attr(test, register_tool(tarpaulin))] #![deny(clippy::all)] #![deny(clippy::pedantic)] #![allow(clippy::module_name_repetitions)] @@ -43,7 +41,7 @@ pub mod libs; mod provider; #[cfg(test)] -#[tarpaulin::skip] +#[cfg(not(tarpaulin_include))] mod test_utils; /// Shortcut for creating a DI container binding for a injectable without a declared @@ -79,6 +77,7 @@ mod test_utils; /// /// di_container_bind!(INinja => Ninja, di_container); /// ``` +#[cfg(not(tarpaulin_include))] #[macro_export] macro_rules! di_container_bind { ($interface: path => $implementation: ty, $di_container: ident) => { @@ -126,6 +125,7 @@ macro_rules! di_container_bind { /// ``` #[cfg(feature = "async")] #[cfg_attr(doc_cfg, doc(cfg(feature = "async")))] +#[cfg(not(tarpaulin_include))] #[macro_export] macro_rules! async_closure { (|$($args: ident),*| { $($inner: stmt);* }) => { -- cgit v1.2.3-18-g5258