fieldname_access/
lib.rs

1use itertools::Itertools;
2use proc_macro::TokenStream;
3use proc_macro2::{Ident, Span, TokenTree};
4use quote::{quote, ToTokens};
5use syn::{
6    parse_macro_input, parse_quote, Attribute, Data, DeriveInput, Expr, ExprLit, Fields,
7    FieldsNamed, Generics, Lit, Meta, Type, TypeGenerics, Visibility, WhereClause,
8};
9
10/// # Description
11///
12/// Derive macro for safe struct field access by their names in runtime.
13///
14/// Also it generates `const FIELDS: [&'static str; FIELDS_COUNT]` constant with struct fields and `field_iter`
15/// method on struct for creating `Iterator` over struct using generated field enum.
16///
17///### Container attributes
18///* `#fieldname_enum(name = "NewName")` - Name of generated enum of possible values
19///
20///```rust
21/// use fieldname_access::FieldnameAccess;
22///
23/// #[derive(FieldnameAccess, Default)]
24/// #[fieldname_enum(name = "NewName")]
25/// struct NamedFieldname {
26///     name: String,
27///     age: i64,
28/// }
29///
30/// let mut instance = NamedFieldname::default();
31/// match instance.field("name").unwrap() {
32///     NewName::String(val) => {}
33///     NewName::I64(val) => {},
34/// }
35/// match instance.field_mut("name").unwrap() {
36///     NewNameMut::String(val) => {}
37///     NewNameMut::I64(val) => {},
38/// }
39///```
40///
41///* `#fieldname_enum(derive = [Debug, Clone], derive_mut = [Debug])` - Derive macroses for generated enums.
42/// `derive` only for enum with immutable references, `derive_mut` only for enum with mutable references.
43/// It can be helpful when you want to derive `Clone` but only for immutable references as mutable are not clonable
44///
45///```rust
46/// use fieldname_access::FieldnameAccess;
47///
48/// #[derive(FieldnameAccess)]
49/// #[fieldname_enum(derive = [Debug, Clone], derive_mut = [Debug])]
50/// struct NamedFieldname {
51///     name: String,
52///     age: i64,
53/// }
54///```
55///
56///* `#fieldname_enum(derive_all = [Debug])` - Derive macroses for immutable and mutable generated enums
57///
58///```rust
59/// use fieldname_access::FieldnameAccess;
60///
61/// #[derive(FieldnameAccess)]
62/// #[fieldname_enum(derive_all = [Debug])]
63/// struct NamedFieldname {
64///     name: String,
65///     age: i64,
66/// }
67///```
68///
69///### Field attributes
70///
71///* `#fieldname = "AmazingAge"` - Name of variant for field in generated enum.
72///It can be helpfull when you want to 'mark' field with specific variant name
73///
74///```rust
75/// use fieldname_access::FieldnameAccess;
76///
77/// #[derive(FieldnameAccess, Default)]
78/// struct NamedFieldname {
79///     name: String,
80///     #[fieldname = "MyAge"]
81///     age: i64,
82///     dog_age: i64
83/// }
84/// let mut instance = NamedFieldname::default();
85/// match instance.field("name").unwrap() {
86///     NamedFieldnameField::String(val) => {}
87///     NamedFieldnameField::MyAge(val) => {}
88///     NamedFieldnameField::I64(val) => {}
89/// }
90/// match instance.field_mut("name").unwrap() {
91///     NamedFieldnameFieldMut::String(val) => {}
92///     NamedFieldnameFieldMut::MyAge(val) => {}
93///     NamedFieldnameFieldMut::I64(val) => {}
94/// }  
95///```
96
97#[proc_macro_derive(FieldnameAccess, attributes(fieldname_enum, fieldname))]
98pub fn fieldname_accessor(inp: TokenStream) -> TokenStream {
99    let inp = parse_macro_input!(inp as DeriveInput);
100    let structure = match inp.data {
101        Data::Struct(ref s) => s,
102        Data::Union(_) => {
103            panic!("FieldnameAccess cannot be used with unions")
104        }
105        Data::Enum(_) => {
106            panic!("FieldnameAccess cannot be used with enums")
107        }
108    };
109    let DeriveInput {
110        ident: struct_ident,
111        vis: visibility,
112        generics,
113        ..
114    } = inp;
115
116    let field_lifetime: syn::GenericParam = parse_quote!('field);
117    let (impl_generics, ty_generics, where_clauses) = generics.split_for_impl();
118
119    let mut enum_generics = generics.clone();
120    enum_generics.params.push(field_lifetime.clone());
121
122    let fields = match &structure.fields {
123        Fields::Named(FieldsNamed { named: x, .. }) => x.to_owned(),
124        Fields::Unnamed(_) | Fields::Unit => {
125            panic!("Nameless fields are not supported")
126        }
127    };
128
129    let field_map = fields
130        .into_iter()
131        .map(|field| {
132            let field_type = field.ty;
133            let field_name = field.ident.expect("Nameless fields are not supported");
134            let variant_ident = if let Some(name) = retrieve_fieldname(&field.attrs) {
135                name
136            } else {
137                let type_str = generate_variant_name(&field_type);
138                Ident::new(&type_str, Span::call_site())
139            };
140            (field_name, field_type, variant_ident)
141        })
142        .collect::<Vec<_>>();
143    let field_list = field_map.iter().map(|(name, _, _)| name.to_string());
144    let field_count = field_map.len();
145
146    let (derive, derive_mut) = if let Some(derives) = retrieve_derives(&inp.attrs, "derive_all") {
147        (Some(derives.clone()), Some(derives))
148    } else {
149        let derive = retrieve_derives(&inp.attrs, "derive");
150        let derive_mut = retrieve_derives(&inp.attrs, "derive_mut");
151        (derive, derive_mut)
152    };
153
154    let value_enum_ident = retrieve_enum_name(&inp.attrs).unwrap_or(Ident::new(
155        &format!("{}Field", struct_ident),
156        Span::call_site(),
157    ));
158    let value_enum_ident_mut = Ident::new(&format!("{}Mut", value_enum_ident), Span::call_site());
159
160    let value_variants = generate_enum_variants(&field_map, false);
161    let value_variants_mut = generate_enum_variants(&field_map, true);
162
163    let match_arms = generate_match_arms(&field_map, &value_enum_ident, false);
164    let match_arms_mut = generate_match_arms(&field_map, &value_enum_ident_mut, true);
165
166    let iter_impl = generate_iter_impl(
167        &visibility,
168        &value_enum_ident,
169        &struct_ident,
170        &ty_generics,
171        &where_clauses,
172        &enum_generics,
173        &field_lifetime,
174    );
175
176    let tokens = quote! {
177        /// Enum with reference to possible field
178        #derive
179        #visibility enum #value_enum_ident #enum_generics {
180            #(#value_variants,)*
181        }
182
183        /// Enum with mutable reference to possible field
184        #derive_mut
185        #visibility enum #value_enum_ident_mut #enum_generics {
186            #(#value_variants_mut,)*
187        }
188
189        #iter_impl
190
191        impl #impl_generics #struct_ident #ty_generics #where_clauses {
192            /// List with all struct fields
193            const FIELDS: [&'static str; #field_count] = [#(#field_list),*];
194
195            /// Method for getting reference to struct field by its name
196            #visibility fn field<#field_lifetime>(&#field_lifetime self, fieldname: &str) -> Option<#value_enum_ident #enum_generics> {
197                match fieldname {
198                    #(#match_arms,)*
199                    _ => None
200                }
201            }
202            /// Method for getting mutable reference to struct field by its name
203            #visibility fn field_mut<#field_lifetime>(&#field_lifetime mut self, fieldname: &str) -> Option<#value_enum_ident_mut #enum_generics> {
204                match fieldname {
205                    #(#match_arms_mut,)*
206                    _ => None
207                }
208            }
209        }
210    };
211    tokens.into()
212}
213
214fn generate_variant_name(ty: &syn::Type) -> String {
215    let type_str = ty.to_token_stream().to_string();
216    shorten_type(type_str)
217}
218
219fn generate_iter_impl(
220    vis: &Visibility,
221    value_enum_ident: &Ident,
222    struct_ident: &Ident,
223    struct_generics: &TypeGenerics,
224    where_clauses: &Option<&WhereClause>,
225    enum_generics: &Generics,
226    enum_lt: &syn::GenericParam,
227) -> proc_macro2::TokenStream {
228    let iter_ident = Ident::new(&format!("{}FieldIter", value_enum_ident), Span::call_site());
229
230    let struct_generic_turbofish = struct_generics.as_turbofish();
231    let struct_ident_turbofish = quote! { #struct_ident #struct_generic_turbofish };
232
233    quote! {
234        #vis struct #iter_ident #enum_generics #where_clauses {
235            idx: usize,
236            inner: &#enum_lt #struct_ident #struct_generics
237        }
238
239        impl #struct_generics #struct_ident #struct_generics #where_clauses {
240            pub fn field_iter<#enum_lt>(&#enum_lt self) -> #iter_ident #enum_generics {
241                #iter_ident {
242                    idx: 0,
243                    inner: self
244                }
245            }
246        }
247
248        impl #enum_generics Iterator for #iter_ident #enum_generics #where_clauses {
249            type Item = (&'static str, #value_enum_ident #enum_generics);
250
251            fn next(&mut self) -> Option<Self::Item> {
252                (self.idx != #struct_ident_turbofish::FIELDS.len()).then(|| {
253                    let field_name = #struct_ident_turbofish::FIELDS[self.idx];
254                    self.idx += 1;
255                    (field_name, self.inner.field(field_name).unwrap())
256                })
257            }
258        }
259    }
260}
261
262fn shorten_type(type_str: String) -> String {
263    let mut short_type = type_str
264        .chars()
265        .skip_while(|c| !c.is_uppercase())
266        .peekable();
267    if short_type.peek().is_some() {
268        let mut complex_type_str = String::new();
269        while let Some(c) = short_type.next() {
270            if c.is_ascii_alphanumeric() {
271                complex_type_str.push(c);
272            }
273            if c == '<' {
274                complex_type_str += &shorten_type(short_type.collect());
275                break;
276            }
277        }
278        complex_type_str
279    } else {
280        let cleaned_str = type_str
281            .chars()
282            .filter(|c| c.is_ascii_alphanumeric())
283            .collect::<String>();
284        cleaned_str[0..1].to_uppercase() + &cleaned_str[1..]
285    }
286}
287
288fn generate_enum_variants(
289    field_map: &[(Ident, syn::Type, Ident)],
290    is_mut: bool,
291) -> Vec<proc_macro2::TokenStream> {
292    field_map
293        .iter()
294        .unique_by(|(_, _, variant_ident)| variant_ident)
295        .map(|(_, field_type, variant_ident)| {
296            if is_mut {
297                quote! {
298                    #variant_ident(&'field mut #field_type)
299                }
300            } else {
301                quote! {
302                    #variant_ident(&'field #field_type)
303                }
304            }
305        })
306        .collect()
307}
308
309fn generate_match_arms(
310    field_map: &[(Ident, Type, Ident)],
311    value_enum_ident: &Ident,
312    is_mut: bool,
313) -> Vec<proc_macro2::TokenStream> {
314    field_map
315        .iter()
316        .map(|(field_name, _, variant_ident)| {
317            let field_name_str = field_name.to_string();
318            if is_mut {
319                quote! {
320                    #field_name_str => Some(#value_enum_ident::#variant_ident(&mut self.#field_name))
321                }
322            } else {
323                quote! {
324                    #field_name_str => Some(#value_enum_ident::#variant_ident(&self.#field_name))
325                }
326            }
327        })
328        .collect()
329}
330
331fn retrieve_enum_name(attrs: &[Attribute]) -> Option<Ident> {
332    if let Some(TokenTree::Literal(lit)) = get_fieldname_enum_val(attrs, "name") {
333        let lit = lit.to_string();
334        Some(Ident::new(&lit[1..lit.len() - 1], Span::call_site()))
335    } else {
336        None
337    }
338}
339
340fn retrieve_derives(attrs: &[Attribute], derive_group: &str) -> Option<proc_macro2::TokenStream> {
341    if let Some(TokenTree::Group(group)) = get_fieldname_enum_val(attrs, derive_group) {
342        let token_stream = group.stream();
343        Some(quote!(#[derive(#token_stream)]))
344    } else {
345        None
346    }
347}
348
349fn retrieve_fieldname(attrs: &[Attribute]) -> Option<Ident> {
350    attrs.iter().find_map(|attr| match &attr.meta {
351        Meta::NameValue(meta_name_value) => {
352            let fieldname_enum_attr = meta_name_value.path.segments.first()?;
353            if fieldname_enum_attr.ident != "fieldname" {
354                return None;
355            }
356            if let Expr::Lit(ExprLit {
357                lit: Lit::Str(ref str),
358                ..
359            }) = meta_name_value.value
360            {
361                Some(Ident::new(&str.value(), Span::call_site()))
362            } else {
363                None
364            }
365        }
366
367        _ => None,
368    })
369}
370
371fn get_fieldname_enum_val(attrs: &[Attribute], attr_name: &str) -> Option<TokenTree> {
372    attrs.iter().find_map(|attr| match &attr.meta {
373        Meta::List(meta_list) => {
374            let fieldname_enum_attr = meta_list.path.segments.first()?;
375            if fieldname_enum_attr.ident != "fieldname_enum" {
376                return None;
377            }
378            meta_list
379                .tokens
380                .clone()
381                .into_iter()
382                .skip_while(|token| token.to_string() != attr_name)
383                .nth(2)
384        }
385        _ => None,
386    })
387}