diff options
| author | Martin Fischer <martin@push-f.com> | 2021-11-25 10:30:54 +0100 | 
|---|---|---|
| committer | Martin Fischer <martin@push-f.com> | 2021-11-26 11:45:53 +0100 | 
| commit | 7489a3c2246e7ea2483446dd2ed3fdbfaf462c1a (patch) | |
| tree | fc2670f56f7cc16567eb10d88f9439abb9098298 | |
| parent | 43950edc4f26a07055fb917fa8bbb262276e2a08 (diff) | |
introduce #[convert] attribute
| -rw-r--r-- | README.md | 55 | ||||
| -rw-r--r-- | src/lib.rs | 15 | ||||
| -rw-r--r-- | src/parse_assoc_type.rs | 8 | ||||
| -rw-r--r-- | src/parse_attrs.rs | 59 | ||||
| -rw-r--r-- | src/trait_sig.rs | 9 | ||||
| -rw-r--r-- | src/transform.rs | 16 | ||||
| -rw-r--r-- | tests/tests.rs | 19 | ||||
| -rw-r--r-- | ui-tests/src/bin/attr_convert_duplicate.rs | 8 | ||||
| -rw-r--r-- | ui-tests/src/bin/attr_convert_duplicate.stderr | 5 | ||||
| -rw-r--r-- | ui-tests/src/bin/qualified_self.stderr | 6 | ||||
| -rw-r--r-- | ui-tests/src/bin/qualified_self_opt.stderr | 6 | 
11 files changed, 182 insertions, 24 deletions
| @@ -169,6 +169,61 @@ trait Client: Sync {  Note that it is important that the `#[dynamize]` attribute comes before the  `#[async_trait]` attribute, since dynamize must run before async_trait. +## Dynamized supertraits + +In Rust a macro only operates on the passed input; it does not have access to +the surrounding source code. This also means that a `#[dynamize]` macro cannot +know which other traits have been dynamized. When you want to dynamize a trait +with a dynamized supertrait, you have to tell dynamize about it with the +`#[dynamized(...)]` attribute: + +```rust ignore +#[dynamize::dynamize] +trait Client { +    type Error: Into<SuperError>; + +    fn get(&self, url: String) -> Result<Vec<u8>, Self::Error>; +} + +#[dynamize::dynamize] +#[dynamized(Client)] +trait ClientWithCache: Client { +    type Error: Into<SuperError>; + +    fn get_with_cache<C: Cache>( +        &self, +        url: String, +        cache: C, +    ) -> Result<Vec<u8>, <Self as ClientWithCache>::Error>; +} +``` + +This results in `DynClientWithCache` having the dynamized `DynClient` supertrait. + +With the above code both traits have independent associated types. So a trait +could implement one trait with one `Error` type and and the other trait with +another `Error` type. If you don't want that to be possible you can change the +second trait to: + +```rust ignore +#[dynamize::dynamize] +#[dynamized(Client)] +#[convert = |x: <Self as Client>::Error| -> SuperError {x.into()}] +trait ClientWithCache: Client { +    fn get_with_cache<C: Cache>( +        &self, +        url: String, +        cache: C, +    ) -> Result<Vec<u8>, <Self as Client>::Error>; +} +``` + +Note that we removed the associated type and are now using the associated type +from the supertrait by qualifying `Self as Client`.  Since the `#[dynamize]` +attribute on the `ClientWithCache` trait however cannot know the associated +type from another trait, we also need to add a `#[convert = ...]` attribute to +tell dynamize how to convert `<Self as Client>::Error>`. +  ## Using dynamize with other collections  Dynamize automatically recognizes collections from the standard library like @@ -93,9 +93,10 @@ pub fn dynamize(_attr: TokenStream, input: TokenStream) -> TokenStream {      }      let mut type_converter = TypeConverter { -        collections: method_attrs.collections, -        assoc_type_conversions: HashMap::new(),          trait_ident: original_trait.ident.clone(), +        assoc_type_conversions: HashMap::new(), +        collections: method_attrs.collections, +        type_conversions: method_attrs.type_conversions,      };      for item in &original_trait.items { @@ -122,9 +123,10 @@ pub fn dynamize(_attr: TokenStream, input: TokenStream) -> TokenStream {      let mut objectifiable_methods: Vec<(Signature, SignatureChanges)> = Vec::new(); -    for item in &original_trait.items { +    for item in &mut original_trait.items {          if let TraitItem::Method(method) = item {              let mut signature = method.sig.clone(); +              match convert_trait_signature(&mut signature, &type_converter) {                  Ok(parsed_method) => objectifiable_methods.push((signature, parsed_method)),                  Err((span, err)) => match err { @@ -169,7 +171,7 @@ pub fn dynamize(_attr: TokenStream, input: TokenStream) -> TokenStream {                          return abort!(span, "dynamize does not support qualified associated types")                      }                      MethodError::Transform(TransformError::SelfQualifiedAsOtherTrait) => { -                        return abort!(span, "dynamize does not support Self qualified as another trait") +                        return abort!(span, "dynamize does not know how to convert this type (you can tell it with #[convert])")                      }                      MethodError::UnconvertedAssocType => {                          return abort!(span, "dynamize does not support associated types here") @@ -471,6 +473,11 @@ impl TypeTransform {                  quote! {#arg.into_iter().map(|(#(#idents),*)| (#(#transforms),*)).collect()}              }              TypeTransform::NoOp => arg, +            TypeTransform::Verbatim(convert) => { +                let ident = &convert.ident; +                let block = &convert.block; +                quote! { {let #ident = #arg; #block }} +            }          }      }  } diff --git a/src/parse_assoc_type.rs b/src/parse_assoc_type.rs index adde237..e52dd14 100644 --- a/src/parse_assoc_type.rs +++ b/src/parse_assoc_type.rs @@ -30,12 +30,12 @@ impl ToTokens for BoxType {      }  } -pub enum DestType<'a> { -    Into(&'a Type), +pub enum DestType { +    Into(Type),      Box(BoxType),  } -impl DestType<'_> { +impl DestType {      pub fn get_dest(&self) -> Type {          match self {              DestType::Into(ty) => (*ty).clone(), @@ -78,7 +78,7 @@ pub fn parse_assoc_type(                          ));                      } -                    return Ok((&assoc_type.ident, DestType::Into(into_type))); +                    return Ok((&assoc_type.ident, DestType::Into(into_type.clone())));                  }              }          } diff --git a/src/parse_attrs.rs b/src/parse_attrs.rs index d9725ff..06a5123 100644 --- a/src/parse_attrs.rs +++ b/src/parse_attrs.rs @@ -1,8 +1,14 @@ -use std::collections::{HashMap, HashSet}; +use std::{ +    collections::{HashMap, HashSet}, +    rc::Rc, +};  use proc_macro2::{Group, TokenStream};  use quote::quote; -use syn::{parenthesized, parse::Parse, Attribute, Error, Ident, LitInt, Token}; +use syn::{ +    braced, parenthesized, parse::Parse, spanned::Spanned, Attribute, Error, Ident, LitInt, Token, +    Type, +};  struct Collection {      pub id: Ident, @@ -32,8 +38,9 @@ impl Parse for Collection {  pub struct TraitAttrs {      pub blanket_impl_attrs: Vec<TokenStream>,      pub dyn_trait_attrs: Vec<TokenStream>, -    pub collections: HashMap<Ident, usize>,      pub dynamized_supertraits: HashSet<Ident>, +    pub collections: HashMap<Ident, usize>, +    pub type_conversions: HashMap<Type, Rc<Convert>>,  }  struct Dynamized { @@ -82,6 +89,21 @@ impl TraitAttrs {                  };                  let tokens = group.stream();                  parsed.dyn_trait_attrs.push(quote! {#[#tokens]}); +            } else if attrs[i].path.is_ident("convert") { +                let attr = attrs.remove(i); +                let convert: Convert = syn::parse2(attr.tokens)?; +                let span = convert.original_type.span(); + +                if parsed +                    .type_conversions +                    .insert(convert.original_type.clone(), Rc::new(convert)) +                    .is_some() +                { +                    return Err(Error::new( +                        span, +                        format_args!("conversion is defined multiple times for this type"), +                    )); +                }              } else if attrs[i].path.is_ident("collection") {                  let attr = attrs.remove(i);                  let coll: Collection = syn::parse2(attr.tokens)?; @@ -118,3 +140,34 @@ impl TraitAttrs {          Ok(parsed)      }  } + +#[derive(Debug, Clone)] +pub struct Convert { +    pub ident: Ident, +    pub original_type: Type, +    pub dest_type: Type, +    pub block: TokenStream, +} + +impl Parse for Convert { +    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> { +        let _: Token![=] = input.parse()?; +        let _: Token![|] = input.parse()?; +        let param: Ident = input.parse()?; +        let _: Token![:] = input.parse()?; +        let original_type: Type = input.parse()?; +        let _: Token![|] = input.parse()?; +        let _: Token![->] = input.parse()?; +        let dest_type: Type = input.parse()?; + +        let inner; +        braced!(inner in input); + +        Ok(Self { +            ident: param, +            original_type, +            dest_type, +            block: inner.parse()?, +        }) +    } +} diff --git a/src/trait_sig.rs b/src/trait_sig.rs index 5d8eddf..a1be44f 100644 --- a/src/trait_sig.rs +++ b/src/trait_sig.rs @@ -1,4 +1,5 @@  use std::collections::HashMap; +use std::rc::Rc;  use proc_macro2::Span;  use syn::{ @@ -8,6 +9,7 @@ use syn::{Ident, Signature, TypeImplTrait};  use crate::match_assoc_type;  use crate::parse_assoc_type::BoxType; +use crate::parse_attrs::Convert;  use crate::syn_utils::{iter_type, trait_bounds};  use crate::transform::{dynamize_function_bounds, TransformError, TypeConverter}; @@ -21,6 +23,7 @@ pub enum TypeTransform {      IntoIterMapCollect(Vec<TypeTransform>),      Iterator(BoxType, Box<TypeTransform>),      Result(Box<TypeTransform>, Box<TypeTransform>), +    Verbatim(Rc<Convert>),  }  #[derive(Debug)] @@ -83,6 +86,7 @@ pub fn convert_trait_signature(          },          ReturnType::Default => TypeTransform::NoOp,      }; +      Ok(SignatureChanges {          return_type,          type_param_transforms, @@ -151,11 +155,12 @@ mod tests {          transform::{TransformError, TypeConverter},      }; -    fn test_converter() -> TypeConverter<'static> { +    fn test_converter() -> TypeConverter {          TypeConverter {              assoc_type_conversions: HashMap::new(),              collections: HashMap::new(),              trait_ident: format_ident!("test"), +            type_conversions: HashMap::new(),          }      } @@ -185,7 +190,7 @@ mod tests {          let mut type_converter = test_converter();          let ident = format_ident!("A");          let dest_inner = Type::Verbatim(quote! {Example}); -        let dest = DestType::Into(&dest_inner); +        let dest = DestType::Into(dest_inner);          type_converter.assoc_type_conversions.insert(ident, dest);          assert!(matches!( diff --git a/src/transform.rs b/src/transform.rs index 75138c9..ae345c8 100644 --- a/src/transform.rs +++ b/src/transform.rs @@ -1,4 +1,4 @@ -use std::collections::HashMap; +use std::{collections::HashMap, rc::Rc};  use proc_macro2::Span;  use quote::quote; @@ -10,14 +10,16 @@ use syn::{  use crate::{      filter_map_assoc_paths, match_assoc_type,      parse_assoc_type::{BoxType, DestType}, +    parse_attrs::Convert,      syn_utils::{iter_path, iter_type, type_arguments_mut},      trait_sig::{MethodError, TypeTransform},  }; -pub struct TypeConverter<'a> { -    pub assoc_type_conversions: HashMap<Ident, DestType<'a>>, +pub struct TypeConverter { +    pub assoc_type_conversions: HashMap<Ident, DestType>,      pub collections: HashMap<Ident, usize>,      pub trait_ident: Ident, +    pub type_conversions: HashMap<Type, Rc<Convert>>,  }  #[derive(Debug)] @@ -30,7 +32,7 @@ pub enum TransformError {      SelfQualifiedAsOtherTrait,  } -impl TypeConverter<'_> { +impl TypeConverter {      /// A return type of Some(1) means that the type implements      /// IntoIterator<Item=T1> and FromIterator<T1>      /// with T1 being its first generic type parameter. @@ -59,6 +61,10 @@ impl TypeConverter<'_> {      }      pub fn convert_type(&self, type_: &mut Type) -> Result<TypeTransform, (Span, TransformError)> { +        if let Some(conv) = self.type_conversions.get(type_) { +            *type_ = conv.dest_type.clone(); +            return Ok(TypeTransform::Verbatim(conv.clone())); +        }          if !iter_type(type_).any(match_assoc_type) {              return Ok(TypeTransform::NoOp);          } @@ -139,7 +145,7 @@ impl TypeConverter<'_> {                          return Ok(dest_type.type_transformation());                      } -                    return Err((path.span(), TransformError::SelfQualifiedAsOtherTrait)); +                    return Err((type_.span(), TransformError::SelfQualifiedAsOtherTrait));                  }              }          } else if let Type::Path(TypePath { path, qself: None }) = type_ { diff --git a/tests/tests.rs b/tests/tests.rs index b75c605..11bf10d 100644 --- a/tests/tests.rs +++ b/tests/tests.rs @@ -297,3 +297,22 @@ trait OneTrait: SomeTrait {      fn test(&self) -> Self::X;  } + +struct SuperError; +trait Cache {} +#[dynamize::dynamize] +trait Client { +    type Error: Into<SuperError>; + +    fn get(&self, url: String) -> Result<Vec<u8>, Self::Error>; +} +#[dynamize::dynamize] +#[dynamized(Client)] +#[convert = |x: <Self as Client>::Error| -> SuperError {x.into()}] +trait ClientWithCache: Client { +    fn get_with_cache<C: Cache>( +        &self, +        url: String, +        cache: C, +    ) -> Result<Vec<u8>, <Self as Client>::Error>; +} diff --git a/ui-tests/src/bin/attr_convert_duplicate.rs b/ui-tests/src/bin/attr_convert_duplicate.rs new file mode 100644 index 0000000..b074d81 --- /dev/null +++ b/ui-tests/src/bin/attr_convert_duplicate.rs @@ -0,0 +1,8 @@ +#[dynamize::dynamize] +#[convert = |x: Foo| -> Bar {x.baz()}] +#[convert = |x: Foo| -> Bar {x.baz()}] +trait Trait { +    fn test(&self) -> Foo; +} + +fn main() {} diff --git a/ui-tests/src/bin/attr_convert_duplicate.stderr b/ui-tests/src/bin/attr_convert_duplicate.stderr new file mode 100644 index 0000000..e4f3622 --- /dev/null +++ b/ui-tests/src/bin/attr_convert_duplicate.stderr @@ -0,0 +1,5 @@ +error: conversion is defined multiple times for this type + --> src/bin/attr_convert_duplicate.rs:3:17 +  | +3 | #[convert = |x: Foo| -> Bar {x.baz()}] +  |                 ^^^ diff --git a/ui-tests/src/bin/qualified_self.stderr b/ui-tests/src/bin/qualified_self.stderr index d8c3a37..87dc0c2 100644 --- a/ui-tests/src/bin/qualified_self.stderr +++ b/ui-tests/src/bin/qualified_self.stderr @@ -1,5 +1,5 @@ -error: dynamize does not support Self qualified as another trait - --> src/bin/qualified_self.rs:7:32 +error: dynamize does not know how to convert this type (you can tell it with #[convert]) + --> src/bin/qualified_self.rs:7:23    |  7 |     fn test(&self) -> <Self as Foo>::X; -  |                                ^^^ +  |                       ^ diff --git a/ui-tests/src/bin/qualified_self_opt.stderr b/ui-tests/src/bin/qualified_self_opt.stderr index dd99406..e20e9c3 100644 --- a/ui-tests/src/bin/qualified_self_opt.stderr +++ b/ui-tests/src/bin/qualified_self_opt.stderr @@ -1,5 +1,5 @@ -error: dynamize does not support Self qualified as another trait - --> src/bin/qualified_self_opt.rs:7:39 +error: dynamize does not know how to convert this type (you can tell it with #[convert]) + --> src/bin/qualified_self_opt.rs:7:29    |  7 |     fn test(&self) -> Option<<Self as Foo>::X>; -  |                                       ^^^ +  |                             ^^ | 
