struct2map_derive/
lib.rs

1//! Implements the functionality to enable conversion between a struct type a map container type
2//! in Rust through the use of a procedural macros.
3#![recursion_limit = "128"]
4
5extern crate proc_macro;
6
7use proc_macro::TokenStream;
8use proc_macro2::Span;
9
10use quote::quote;
11use syn::{Data, DeriveInput, Fields, Ident, Type};
12
13use std::collections::BTreeMap;
14
15/// Implements the functionality for converting entries in a BTreeMap into attributes and values of a
16/// struct. It will consume a tokenized version of the initial struct declaration, and use code
17/// generation to implement the `FromMap` trait for instantiating the contents of the struct.
18#[proc_macro_derive(FromMap)]
19pub fn from_map(input: TokenStream) -> TokenStream {
20    let ast = syn::parse_macro_input!(input as DeriveInput);
21
22    // parse out all the field names in the struct as `Ident`s
23    let fields = match ast.data {
24        Data::Struct(st) => st.fields,
25        _ => panic!("Implementation must be a struct"),
26    };
27    let idents: Vec<&Ident> = fields
28        .iter()
29        .filter_map(|field| field.ident.as_ref())
30        .collect::<Vec<&Ident>>();
31
32    // convert all the field names into strings
33    let keys: Vec<String> = idents
34        .clone()
35        .iter()
36        .map(|ident| ident.to_string())
37        .collect::<Vec<String>>();
38
39    // parse out all the primitive types in the struct as Idents
40    let typecalls: Vec<Ident> = fields
41        .iter()
42        .map(|field| match field.ty.clone() {
43            Type::Path(typepath) => {
44                // TODO: options and results
45                // TODO: vecs
46                // TODO: genericized numerics
47
48                // get the type of the specified field, lowercase
49                let typename: String = quote! {#typepath}.to_string().to_lowercase();
50
51                // initialize new Ident for codegen
52                Ident::new(&typename, Span::mixed_site())
53            }
54            _ => unimplemented!(),
55        })
56        .collect::<Vec<Ident>>();
57
58    // get the name identifier of the struct input AST
59    let name: &Ident = &ast.ident;
60    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
61
62    // start codegen of a generic or non-generic impl for the given struct using quasi-quoting
63    let tokens = quote! {
64        impl #impl_generics FromMap for #name #ty_generics #where_clause {
65
66            /* FIXME
67            fn from_stringmap(mut hashmap: StringMap) -> #name {
68                let mut settings = #name::default();
69                #(
70                    match hashmap.entry(String::from(#keys)) {
71                        ::std::collections::hash_map::Entry::Occupied(entry) => {
72                            let value = match entry.get() {
73                                Some(val) => val.parse::<#typecalls>().unwrap(),
74                                None => unreachable!()
75                            };
76                            settings.#idents = value;
77                        },
78                        _ => unreachable!()
79                    }
80                )*
81                settings
82            }
83            */
84
85            fn from_genericmap(mut hashmap: GenericMap) -> #name {
86                let mut settings = #name::default();
87                #(
88                    match hashmap.entry(String::from(#keys)) {
89                        ::std::collections::btree_map::Entry::Occupied(entry) => {
90                            // parse out primitive value from generic type using typed call
91                            let value = match entry.get().#typecalls() {
92                                Some(val) => val,
93                                None => panic!("Cannot parse out map entry")
94                            };
95                            settings.#idents = value;
96                        },
97                        _ => (),
98                    }
99                )*
100                settings
101            }
102        }
103    };
104    TokenStream::from(tokens)
105}
106
107/// Converts a given input struct into a BTreeMap where the keys are the attribute names assigned to
108/// the values of the entries.
109#[proc_macro_derive(ToMap, attributes(rename))]
110pub fn to_map(input_struct: TokenStream) -> TokenStream {
111    let ast = syn::parse_macro_input!(input_struct as DeriveInput);
112
113    // check for struct type and parse out fields
114    let fields = match ast.data {
115        Data::Struct(st) => st.fields,
116        _ => panic!("Implementation must be a struct"),
117    };
118
119    // before unrolling out more, get mapping of any renaming needed to be done
120    let rename_map = parse_rename_attrs(&fields);
121
122    // parse out all the field names in the struct as `Ident`s
123    let idents: Vec<&Ident> = fields
124        .iter()
125        .filter_map(|field| field.ident.as_ref())
126        .collect::<Vec<&Ident>>();
127
128    // convert all the field names into strings
129    let keys: Vec<String> = idents
130        .clone()
131        .iter()
132        .map(|ident| ident.to_string())
133        .map(|name| match rename_map.contains_key(&name) {
134            true => rename_map.get(&name).unwrap().clone(),
135            false => name,
136        })
137        .collect::<Vec<String>>();
138
139    // get the name identifier of the struct input AST
140    let name: &Ident = &ast.ident;
141    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
142
143    // start codegen for to_hashmap functionality that converts a struct into a hashmap
144    let tokens = quote! {
145
146        impl #impl_generics ToMap for #name #ty_generics #where_clause {
147
148            fn to_stringmap(mut input_struct: #name) -> structmap::StringMap {
149                let mut map = structmap::StringMap::new();
150                #(
151                    map.insert(#keys.to_string(), input_struct.#idents.to_string());
152                )*
153                map
154            }
155
156            fn to_genericmap(mut input_struct: #name) -> structmap::GenericMap {
157                let mut map = structmap::GenericMap::new();
158                #(
159                    map.insert(#keys.to_string(), structmap::value::Value::new(input_struct.#idents));
160                )*
161                map
162            }
163        }
164    };
165    TokenStream::from(tokens)
166}
167
168/// Helper method used to parse out any `rename` attribute definitions in a struct
169/// marked with the ToMap trait, returning a mapping between the original field name
170/// and the one being changed for later use when doing codegen.
171fn parse_rename_attrs(fields: &Fields) -> BTreeMap<String, String> {
172    let mut rename: BTreeMap<String, String> = BTreeMap::new();
173    match fields {
174        Fields::Named(_) => {
175            // iterate over fields available and attributes
176            for field in fields.iter() {
177                for attr in field.attrs.iter() {
178                    // parse original struct field name
179                    let field_name = field.ident.as_ref().unwrap().to_string();
180                    if rename.contains_key(&field_name) {
181                        panic!("Cannot redefine field name multiple times");
182                    }
183
184                    // parse out name value pairs in attributes
185                    // first get `lst` in #[rename(lst)]
186                    match attr.parse_meta() {
187                        Ok(syn::Meta::List(lst)) => {
188                            // then parse key-value name
189                            match lst.nested.first() {
190                                Some(syn::NestedMeta::Meta(syn::Meta::NameValue(nm))) => {
191                                    // check path to be = `name`
192                                    let path = nm.path.get_ident().unwrap().to_string();
193                                    if path != "name" {
194                                        panic!("Must be `#[rename(name = 'VALUE')]`");
195                                    }
196
197                                    let lit = match &nm.lit {
198                                        syn::Lit::Str(val) => val.value(),
199                                        _ => {
200                                            panic!("Must be `#[rename(name = 'VALUE')]`");
201                                        }
202                                    };
203                                    rename.insert(field_name, lit);
204                                }
205                                _ => {
206                                    panic!("Must be `#[rename(name = 'VALUE')]`");
207                                }
208                            }
209                        }
210                        _ => {
211                            panic!("Must be `#[rename(name = 'VALUE')]`");
212                        }
213                    }
214                }
215            }
216        }
217        _ => {
218            panic!("Must have named fields");
219        }
220    }
221    rename
222}