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>; - | ^^^ + | ^^ |