diff options
author | HampusM <hampus@hampusmat.com> | 2023-03-18 17:14:42 +0100 |
---|---|---|
committer | HampusM <hampus@hampusmat.com> | 2023-03-18 17:15:30 +0100 |
commit | c48271aef7e6b0819c497f302127c161845a83d7 (patch) | |
tree | a18d7b5fc8e017b4b7e0917a55534b28a01fe57d /macros/src/mock.rs | |
parent | 2ca8017deebe7bfe5aac368aead777a2c4910ca2 (diff) |
refactor: rewrite the mock macro as a procedural macro
Diffstat (limited to 'macros/src/mock.rs')
-rw-r--r-- | macros/src/mock.rs | 313 |
1 files changed, 313 insertions, 0 deletions
diff --git a/macros/src/mock.rs b/macros/src/mock.rs new file mode 100644 index 0000000..88d3434 --- /dev/null +++ b/macros/src/mock.rs @@ -0,0 +1,313 @@ +use proc_macro2::{Ident, Span, TokenStream}; +use proc_macro_error::{abort_call_site, ResultExt}; +use quote::{format_ident, quote, ToTokens}; +use syn::{ + parse2, + AngleBracketedGenericArguments, + FnArg, + GenericArgument, + GenericParam, + ImplItemMethod, + Lifetime, + Pat, + Path, + PathSegment, + Receiver, + ReturnType, + Signature, + Token, + TraitItemMethod, + Type, + TypePath, + TypeReference, + Visibility, +}; + +use crate::expectation::create_expectation_ident; +use crate::syn_ext::{ + AngleBracketedGenericArgumentsExt, + GenericArgumentExt, + IsMut, + PathExt, + PathSegmentExt, + ReturnTypeExt, + SignatureExt, + TypePathExt, + TypeReferenceExt, + VisibilityExt, + WithColons, + WithLeadingColons, +}; +use crate::util::create_unit_type_tuple; + +pub struct Mock +{ + ident: Ident, + mocked_trait: TypePath, + expectations_fields: Vec<ExpectationsField>, + item_methods: Vec<TraitItemMethod>, +} + +impl Mock +{ + pub fn new( + ident: Ident, + mocked_trait: TypePath, + item_methods: &[TraitItemMethod], + ) -> Self + { + let expectations_fields = item_methods + .iter() + .map(|method_item| { + let generic_args = method_item + .sig + .generics + .params + .iter() + .filter_map(|generic_param| match generic_param { + GenericParam::Type(_) => Some(GenericArgument::Type( + Type::Tuple(create_unit_type_tuple()), + )), + GenericParam::Lifetime(_) => { + Some(GenericArgument::Lifetime(Lifetime { + apostrophe: Span::call_site(), + ident: format_ident!("static"), + })) + } + GenericParam::Const(_) => None, + }) + .collect::<Vec<_>>(); + + ExpectationsField { + field_ident: format_ident!("{}_expectations", method_item.sig.ident), + expectation_ident: create_expectation_ident( + &ident, + &method_item.sig.ident, + ), + generic_args, + } + }) + .collect::<Vec<_>>(); + + Self { + ident, + mocked_trait, + expectations_fields, + item_methods: item_methods.to_vec(), + } + } +} + +impl ToTokens for Mock +{ + fn to_tokens(&self, tokens: &mut TokenStream) + { + let Self { + ident, + mocked_trait, + expectations_fields, + item_methods, + } = self; + + let expectations_field_idents = expectations_fields + .iter() + .map(|expectations_field| expectations_field.field_ident.clone()); + + let mock_functions = item_methods + .iter() + .map(|item_method| create_mock_function(item_method.clone())); + + let expect_functions = item_methods + .iter() + .map(|item_method| create_expect_function(&self.ident, &item_method.clone())) + .collect::<Vec<_>>(); + + quote! { + pub struct #ident + { + #(#expectations_fields),* + } + + impl #ident + { + pub fn new() -> Self + { + Self { + #( + #expectations_field_idents: ::std::collections::HashMap::new() + ),* + } + } + + #(#expect_functions)* + } + + impl #mocked_trait for #ident { + #( + #mock_functions + )* + } + } + .to_tokens(tokens); + } +} + +struct ExpectationsField +{ + field_ident: Ident, + expectation_ident: Ident, + generic_args: Vec<GenericArgument>, +} + +impl ToTokens for ExpectationsField +{ + fn to_tokens(&self, tokens: &mut TokenStream) + { + let Self { + field_ident, + expectation_ident, + generic_args, + } = self; + + quote! { + #field_ident: ::std::collections::HashMap< + Vec<::ridicule::__private::type_id::TypeID>, + #expectation_ident<#(#generic_args),*> + > + } + .to_tokens(tokens); + } +} + +fn create_mock_function(item_method: TraitItemMethod) -> ImplItemMethod +{ + let func_ident = &item_method.sig.ident; + + let type_param_idents = item_method + .sig + .generics + .type_params() + .map(|type_param| type_param.ident.clone()) + .collect::<Vec<_>>(); + + let args = item_method + .sig + .inputs + .iter() + .map(|fn_arg| match fn_arg { + FnArg::Receiver(_) => format_ident!("self"), + FnArg::Typed(pat_type) => { + let Pat::Ident(pat_ident) = pat_type.pat.as_ref() else { + abort_call_site!("Unsupport argument pattern"); + }; + + pat_ident.ident.clone() + } + }) + .collect::<Vec<_>>(); + + let expectations_field = format_ident!("{func_ident}_expectations"); + + let ids = quote! { + let ids = vec![ + #(::ridicule::__private::type_id::TypeID::of::<#type_param_idents>()),* + ]; + }; + + ImplItemMethod { + attrs: item_method.attrs, + vis: Visibility::Inherited, + defaultness: None, + sig: item_method.sig.clone(), + block: parse2(quote! { + { + #ids + + let expectation = self + .#expectations_field + .get(&ids) + .expect(concat!( + "No expectation found for function ", + stringify!(#func_ident) + )) + .with_generic_params::<#(#type_param_idents),*>(); + + let Some(returning) = &expectation.returning else { + panic!(concat!( + "Expectation for function", + stringify!(#func_ident), + " is missing a function to call") + ); + }; + + returning(#(#args),*) + } + }) + .unwrap_or_abort(), + } +} + +fn create_expect_function(mock: &Ident, item_method: &TraitItemMethod) -> ImplItemMethod +{ + let signature = Signature::new( + format_ident!("expect_{}", item_method.sig.ident), + item_method.sig.generics.clone(), + [FnArg::Receiver(Receiver { + attrs: vec![], + reference: Some((<Token![&]>::default(), None)), + mutability: Some(<Token![mut]>::default()), + self_token: <Token![self]>::default(), + })], + ReturnType::new(Type::Reference(TypeReference::new( + None, + IsMut::Yes, + Type::Path(TypePath::new(Path::new( + WithLeadingColons::No, + [PathSegment::new( + create_expectation_ident(mock, &item_method.sig.ident), + Some(AngleBracketedGenericArguments::new( + WithColons::No, + item_method.sig.generics.params.iter().map(|generic_param| { + GenericArgument::from_generic_param(generic_param.clone()) + }), + )), + )], + ))), + ))), + ); + + let type_param_idents = item_method + .sig + .generics + .type_params() + .map(|type_param| type_param.ident.clone()) + .collect::<Vec<_>>(); + + let expectation = create_expectation_ident(mock, &item_method.sig.ident); + + let expectations_field = format_ident!("{}_expectations", item_method.sig.ident); + + ImplItemMethod { + attrs: item_method.attrs.clone(), + vis: Visibility::new_pub_crate(), + defaultness: None, + sig: signature, + block: parse2(quote! {{ + let ids = vec![ + #(::ridicule::__private::type_id::TypeID::of::<#type_param_idents>()),* + ]; + + let expectation = + #expectation::<#(#type_param_idents),*>::new() + .strip_generic_params(); + + self.#expectations_field.insert(ids.clone(), expectation); + + self.#expectations_field + .get_mut(&ids) + .unwrap() + .with_generic_params_mut() + }}) + .unwrap_or_abort(), + } +} |