derive_into_owned/
lib.rs

1//! # derive_into_owned
2//!
3//! This crate supports deriving two different methods (not traits):
4//!
5//!  * `IntoOwned`
6//!  * `Borrowed`
7//!
8//! These were first created to help out with types generated by [`quick_protobuf`] which generates
9//! structs with [`Cow`] fields. It is entirely possible that this crate is not needed at all and
10//! that there exists better alternatives to what this crate aims to support.
11//!
12//! ## Definitions, naming
13//!
14//! "Cow-alike" is used to mean a type that:
15//!
16//!  * has a lifetime specifier, like `Foo<'a>`
17//!  * has an implementation for `fn into_owned(self) -> Foo<'static>`
18//!
19//! This is a bit of a bad name as [`Cow`] itself has a different kind of `into_owned` which
20//! returns the owned version of the generic type parameter. I am open to suggestions for both
21//! "Cow-alike" and `into_owned`.
22//!
23//! ## `IntoOwned`
24//!
25//! `#[derive(IntoOwned)]` implements a method `fn into_owned(self) -> Foo<'static>` for type
26//! `Foo<'a>` which contains [`Cow`] or "Cow-alike" fields. The method returns a version with
27//! `'static` lifetime which means the value owns all of it's data. This is useful if you are
28//! for example, working with [`tokio-rs`] which currently requires types to be `'static`.
29//!
30//! ## `Borrowed`
31//!
32//! `#[derive(Borrowed)]` implements a method `fn borrowed<'b>(&'b self) -> Foo<'b>` for type
33//! `Foo<'a>`. This is useful in case you need to transform the value into another type using
34//! std conversions like [`From`], but you don't want to clone the data in the process. Note that
35//! the all the fields that are not [`Cow`] or "Cow-alike" are just cloned, and new vectors are
36//! collected, so this yields savings only when you manage to save big chunks of memory.
37//!
38//! ## Limitations
39//!
40//! Currently only the types I needed are supported and this might be a rather limited set of
41//! functionality. If you find that this does not work in your case please file an issue at [project
42//! repository](https://github.com/koivunej/derive-into-owned/issues).
43//!
44//! [`quick_protobuf`]: https://github.com/tafia/quick-protobuf/
45//! [`tokio-rs`]: https://tokio.rs
46//! [`Cow`]: https://doc.rust-lang.org/std/borrow/enum.Cow.html
47//! [`From`]: https://doc.rust-lang.org/std/convert/trait.From.html
48
49use quote::{format_ident, quote};
50
51use proc_macro::TokenStream;
52use syn::{parse_macro_input, DeriveInput};
53
54mod field_kind;
55mod helpers;
56
57use field_kind::FieldKind;
58
59#[proc_macro_derive(IntoOwned)]
60pub fn into_owned(input: TokenStream) -> TokenStream {
61    let ast = parse_macro_input!(input as DeriveInput);
62
63    let expanded = impl_with_generator(&ast, IntoOwnedGen);
64
65    TokenStream::from(expanded)
66}
67
68#[proc_macro_derive(Borrowed)]
69pub fn borrowed(input: TokenStream) -> TokenStream {
70    let ast = parse_macro_input!(input as DeriveInput);
71
72    let expanded = impl_with_generator(&ast, BorrowedGen);
73
74    TokenStream::from(expanded)
75}
76
77fn impl_with_generator<G: BodyGenerator>(
78    ast: &syn::DeriveInput,
79    gen: G,
80) -> proc_macro2::TokenStream {
81    // this is based heavily on https://github.com/asajeffrey/deep-clone/blob/master/deep-clone-derive/lib.rs
82    let name = &ast.ident;
83
84    let borrowed_params = gen.quote_borrowed_params(ast);
85    let borrowed = if borrowed_params.is_empty() {
86        quote! {}
87    } else {
88        quote! { < #(#borrowed_params),* > }
89    };
90
91    let params = gen.quote_type_params(ast);
92    let params = if params.is_empty() {
93        quote! {}
94    } else {
95        quote! { < #(#params),* > }
96    };
97
98    let owned_params = gen.quote_rhs_params(ast);
99    let owned = if owned_params.is_empty() {
100        quote! {}
101    } else {
102        quote! { < #(#owned_params),* > }
103    };
104
105    let body = match ast.data {
106        syn::Data::Struct(ref body) => {
107            let inner = gen.visit_struct(body);
108            quote! { #name #inner }
109        }
110        syn::Data::Enum(ref body) => {
111            let cases = body.variants.iter().map(|variant| {
112                let unqualified_ident = &variant.ident;
113                let ident = quote! { #name::#unqualified_ident };
114
115                gen.visit_enum_data(ident, variant)
116            });
117            quote! { match self { #(#cases),* } }
118        }
119        syn::Data::Union(_) => todo!("unions are not supported (5)"),
120    };
121
122    gen.combine_impl(borrowed, name, params, owned, body)
123}
124
125/// Probably not the best abstraction
126trait BodyGenerator {
127    fn quote_borrowed_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
128        let borrowed_lifetime_params = ast.generics.lifetimes().map(|alpha| quote! { #alpha });
129        let borrowed_type_params = ast.generics.type_params().map(|ty| quote! { #ty });
130        borrowed_lifetime_params
131            .chain(borrowed_type_params)
132            .collect::<Vec<_>>()
133    }
134
135    fn quote_type_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
136        ast.generics
137            .lifetimes()
138            .map(|alpha| quote! { #alpha })
139            .chain(ast.generics.type_params().map(|ty| {
140                let ident = &ty.ident;
141                quote! { #ident }
142            }))
143            .collect::<Vec<_>>()
144    }
145
146    fn quote_rhs_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
147        let owned_lifetime_params = ast.generics.lifetimes().map(|_| quote! { 'static });
148        let owned_type_params = ast.generics.type_params().map(|ty| {
149            let ident = &ty.ident;
150            quote! { #ident }
151        });
152        owned_lifetime_params
153            .chain(owned_type_params)
154            .collect::<Vec<_>>()
155    }
156
157    fn visit_struct(&self, data: &syn::DataStruct) -> proc_macro2::TokenStream;
158    fn visit_enum_data(
159        &self,
160        variant: proc_macro2::TokenStream,
161        data: &syn::Variant,
162    ) -> proc_macro2::TokenStream;
163    fn combine_impl(
164        &self,
165        borrows: proc_macro2::TokenStream,
166        name: &syn::Ident,
167        rhs_params: proc_macro2::TokenStream,
168        owned: proc_macro2::TokenStream,
169        body: proc_macro2::TokenStream,
170    ) -> proc_macro2::TokenStream;
171}
172
173struct IntoOwnedGen;
174
175impl BodyGenerator for IntoOwnedGen {
176    fn visit_struct(&self, data: &syn::DataStruct) -> proc_macro2::TokenStream {
177        // Helper ternary to avoid Option<bool>
178        enum Fields {
179            Named,
180            Tuple,
181            Unit,
182        }
183
184        use Fields::*;
185
186        let fields_kind = data
187            .fields
188            .iter()
189            .next()
190            .map(|field| if field.ident.is_some() { Named } else { Tuple })
191            .unwrap_or(Unit);
192
193        match fields_kind {
194            Named => {
195                let fields = data.fields.iter().map(|field| {
196                    let ident = field.ident.as_ref().expect("unexpected unnamed field");
197                    let field_ref = quote! { self.#ident };
198                    let code = FieldKind::resolve(&field.ty).move_or_clone_field(&field_ref);
199                    quote! { #ident: #code }
200                });
201                quote! { { #(#fields),* } }
202            }
203            Tuple => {
204                let fields = data.fields.iter().enumerate().map(|(index, field)| {
205                    let index = syn::Index::from(index);
206                    let index = quote! { self.#index };
207                    FieldKind::resolve(&field.ty).move_or_clone_field(&index)
208                });
209                quote! { ( #(#fields),* ) }
210            }
211            Unit => {
212                quote! {}
213            }
214        }
215    }
216
217    fn visit_enum_data(
218        &self,
219        ident: proc_macro2::TokenStream,
220        variant: &syn::Variant,
221    ) -> proc_macro2::TokenStream {
222        if variant.fields.is_empty() {
223            return quote!(#ident => #ident);
224        }
225
226        let fields_are_named = variant.fields.iter().any(|field| field.ident.is_some());
227
228        if fields_are_named {
229            let named_fields = variant
230                .fields
231                .iter()
232                .filter_map(|field| field.ident.as_ref());
233
234            let cloned = variant.fields.iter().map(|field| {
235                let ident = field.ident.as_ref().unwrap();
236                let ident = quote!(#ident);
237                let code = FieldKind::resolve(&field.ty).move_or_clone_field(&ident);
238                quote! { #ident: #code }
239            });
240            quote! { #ident { #(#named_fields),* } => #ident { #(#cloned),* } }
241        } else {
242            let unnamed_fields = &variant
243                .fields
244                .iter()
245                .enumerate()
246                .filter(|(_, field)| field.ident.is_none())
247                .map(|(index, _)| format_ident!("arg_{}", index))
248                .collect::<Vec<_>>();
249
250            let cloned = unnamed_fields
251                .iter()
252                .zip(variant.fields.iter())
253                .map(|(ident, field)| {
254                    let ident = quote! { #ident };
255                    FieldKind::resolve(&field.ty).move_or_clone_field(&ident)
256                })
257                .collect::<Vec<_>>();
258
259            quote! { #ident ( #(#unnamed_fields),* ) => #ident ( #(#cloned),* ) }
260        }
261    }
262
263    fn combine_impl(
264        &self,
265        borrowed: proc_macro2::TokenStream,
266        name: &syn::Ident,
267        params: proc_macro2::TokenStream,
268        owned: proc_macro2::TokenStream,
269        body: proc_macro2::TokenStream,
270    ) -> proc_macro2::TokenStream {
271        quote! {
272            impl #borrowed #name #params {
273                /// Returns a version of `self` with all fields converted to owning versions.
274                pub fn into_owned(self) -> #name #owned { #body }
275            }
276        }
277    }
278}
279
280struct BorrowedGen;
281
282impl BodyGenerator for BorrowedGen {
283    fn quote_rhs_params(&self, ast: &syn::DeriveInput) -> Vec<proc_macro2::TokenStream> {
284        let owned_lifetime_params = ast.generics.lifetimes().map(|_| quote! { '__borrowedgen });
285        let owned_type_params = ast.generics.type_params().map(|ty| {
286            let ident = &ty.ident;
287            quote! { #ident }
288        });
289        owned_lifetime_params
290            .chain(owned_type_params)
291            .collect::<Vec<_>>()
292    }
293
294    fn visit_struct(&self, data: &syn::DataStruct) -> proc_macro2::TokenStream {
295        let fields = data.fields.iter().map(|field| {
296            let ident = field.ident.as_ref().expect("this fields has no ident (4)");
297            let field_ref = quote! { self.#ident };
298            let code = FieldKind::resolve(&field.ty).borrow_or_clone(&field_ref);
299            quote! { #ident: #code }
300        });
301        quote! { { #(#fields),* } }
302    }
303
304    fn visit_enum_data(
305        &self,
306        ident: proc_macro2::TokenStream,
307        variant: &syn::Variant,
308    ) -> proc_macro2::TokenStream {
309        if variant.fields.is_empty() {
310            return quote!(#ident => #ident);
311        }
312
313        let fields_are_named = variant.fields.iter().any(|field| field.ident.is_some());
314        if fields_are_named {
315            let idents = variant
316                .fields
317                .iter()
318                .map(|field| field.ident.as_ref().expect("this fields has no ident (5)"));
319            let cloned = variant.fields.iter().map(|field| {
320                let ident = field.ident.as_ref().expect("this fields has no ident (6)");
321                let ident = quote! { #ident };
322                let code = FieldKind::resolve(&field.ty).borrow_or_clone(&ident);
323                quote! { #ident: #code }
324            });
325            quote! { #ident { #(ref #idents),* } => #ident { #(#cloned),* } }
326        } else {
327            let idents = (0..variant.fields.len())
328                .map(|index| quote::format_ident!("x{}", index))
329                .collect::<Vec<_>>();
330            let cloned = idents
331                .iter()
332                .zip(variant.fields.iter())
333                .map(|(ident, field)| {
334                    let ident = quote! { #ident };
335                    FieldKind::resolve(&field.ty).borrow_or_clone(&ident)
336                })
337                .collect::<Vec<_>>();
338            quote! { #ident ( #(ref #idents),* ) => #ident ( #(#cloned),* ) }
339        }
340    }
341
342    fn combine_impl(
343        &self,
344        borrowed: proc_macro2::TokenStream,
345        name: &syn::Ident,
346        params: proc_macro2::TokenStream,
347        owned: proc_macro2::TokenStream,
348        body: proc_macro2::TokenStream,
349    ) -> proc_macro2::TokenStream {
350        quote! {
351            impl #borrowed #name #params {
352                /// Returns a clone of `self` that shares all the "Cow-alike" data with `self`.
353                pub fn borrowed<'__borrowedgen>(&'__borrowedgen self) -> #name #owned { #body }
354            }
355        }
356    }
357}