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