influxdb2-structmap-derive 0.1.5

Proc derive macro for structmap crate
Documentation
//! Implements the functionality to enable conversion between a struct type a 
//! map container type in Rust through the use of a procedural macros.
#![recursion_limit = "128"]

extern crate proc_macro;

use itertools::izip;
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Ident};

/// Implements the functionality for converting entries in a BTreeMap into 
/// attributes and values of a struct. It will consume a tokenized version of 
/// the initial struct declaration, and use code generation to implement the 
/// `FromMap` trait for instantiating the contents of the struct.
#[proc_macro_derive(FromMap)]
pub fn from_map(input: TokenStream) -> TokenStream {
    let ast = syn::parse_macro_input!(input as DeriveInput);

    // parse out all the field names in the struct as `Ident`s
    let fields = match ast.data {
        Data::Struct(st) => st.fields,
        _ => panic!("Implementation must be a struct"),
    };
    let idents: Vec<&Ident> = fields
        .iter()
        .filter_map(|field| field.ident.as_ref())
        .collect::<Vec<&Ident>>();

    // convert all the field names into strings
    let keys: Vec<String> = idents
        .clone()
        .iter()
        .map(|ident| ident.to_string())
        .collect::<Vec<String>>();

    let typenames = fields
        .iter()
        .map(|field| { /*match field.ty.clone() {
            Type::Path(typepath) => {
                typepath
                /*
                let typename = quote! {#typepath}.to_string();
                typename
                */
            }
            _ => unimplemented!(),
            */
            let t = field.ty.clone();
            let s = quote! {#t}.to_string();
            s
        })
        .collect::<Vec<String>>();

    // parse out all the primitive types in the struct as Idents
    /*
    let typecalls: Vec<Ident> = fields
        .iter()
        .map(|field| match field.ty.clone() {
            Type::Path(typepath) => {
                // TODO: options and results
                // TODO: vecs
                // TODO: genericized numerics

                // get the type of the specified field, lowercase
                let typename: String = quote! {#typepath}.to_string().to_lowercase();

                // initialize new Ident for codegen
                Ident::new(&typename, Span::mixed_site())
            }
            _ => unimplemented!(),
        })
        .collect::<Vec<Ident>>();
    */

    // get the name identifier of the struct input AST
    let name: &Ident = &ast.ident;
    let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();

    let datetime_re = regex::Regex::new(r"DateTime").unwrap();
    let duration_re = regex::Regex::new(r"Duration").unwrap();
    let base64_re = regex::Regex::new(r"Vec").unwrap();
    let mut assignments = Vec::new();
    for (key, typename, ident) in izip!(keys, typenames, idents) {
        match &typename[..] {
            "f64" => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::Double(v) = entry.get() {
                                settings.#ident = *v;
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            "i64" => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::Long(v) = entry.get() {
                                settings.#ident = *v;
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            "u64" => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::UnsignedLong(v) = entry.get() {
                                settings.#ident = *v;
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            "bool" => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::Bool(v) = entry.get() {
                                settings.#ident = *v;
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            "String" => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::String(v) = entry.get() {
                                settings.#ident = v.clone();
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            x if duration_re.is_match(x) => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::Duration(v) = entry.get() {
                                settings.#ident = *v;
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            x if datetime_re.is_match(x) => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::TimeRFC(v) = entry.get() {
                                settings.#ident = *v;
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            x if base64_re.is_match(x) => {
                assignments.push(quote! {
                    let mut key = String::from(#key);
                    if !hashmap.contains_key(&key) {
                        key = format!("_{}", key);
                    }
                    match hashmap.entry(key) {
                        ::std::collections::btree_map::Entry::Occupied(entry) => {
                            if let Value::Base64Binary(v) = entry.get() {
                                settings.#ident = *v;
                            }
                        },
                        _ => panic!("Cannot parse out map entry"),
                    }
                })
            }
            x => {
                panic!("{} is not handled", x);
            }
        }
    }

    // start codegen of a generic or non-generic impl for the given struct using quasi-quoting
    let tokens = quote! {
        use influxdb2_structmap::value::Value;
        use influxdb2_structmap::{GenericMap};

        impl #impl_generics FromMap for #name #ty_generics #where_clause {

            fn from_genericmap(mut hashmap: GenericMap) -> #name {
                let mut settings = #name::default();

                #(
                    #assignments
                )*

                settings
            }

        }
    };
    TokenStream::from(tokens)
}

#[cfg(test)]
mod tests {
    #[test]
    fn ui() {
        let t = trybuild::TestCases::new();
        t.pass("tests/struct.rs");
    }
}