glitchup_derive 0.4.0

Helper macros for glitchup
Documentation
extern crate proc_macro;

use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput, Data, Fields,
          DataStruct, FieldsNamed, punctuated::Punctuated, Field,
          PathSegment};

/// Derives the `MutConfig` trait for any struct.
#[proc_macro_derive(MutConfig)]
pub fn derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);

    let sname = &ast.ident;
    let data = &ast.data;

    if sname.to_string().find("Config").is_none() {
        panic!("Please name the struct in a format like <name>Config (ex. MainConfig)");
    };

    let fields = filter_ignore_out(extract_fields(&data));
    let types = extract_types(&data);

    // generates an appropriate map insertion according to the field
    let insertions = (0..fields.len()).map(|i| {
        let fname = &fields[i].ident;

        let oval = into_oval(&fields[i], &types[i]);

        quote! {
            map.insert(String::from(stringify!(#fname)), #oval);
        }
    });

    let allinserts = quote! {
        #(#insertions)*
    };

    let expandify = quote! {
        impl MutConfig for #sname {
            fn to_hashmap(&self) -> HashMap<String, MutOptionVal> {
                use MutOptionVal::*;
                let mut map = HashMap::new();

                #allinserts

                map
            }
        }
    };

    expandify.into()
}

/// Extracts the names of the fields of the struct
fn extract_fields(data: &Data) -> &Punctuated<Field, syn::token::Comma>{
    if let Data::Struct(DataStruct {
        fields: Fields::Named(FieldsNamed {
            ref named,
            ..
        }), ..
    }) = data {
        named
    } else {
        unimplemented!()
    }
}

fn not_ignored(field: &Field) -> bool {
    for attr in &field.attrs {
        let segs = &attr.path.segments;

        for seg in segs {
            let attrname = seg.ident.to_string();

            if attrname == "ignore" {return false;};
        }
    };

    true
}

fn filter_ignore_out(fields: &Punctuated<Field, syn::token::Comma>) -> Vec<&Field> {
    fields.iter().filter(|f| not_ignored(*f)).collect()
}

/// Extracts the types of the fields of the struct
fn extract_types(data: &Data) -> Vec<&syn::PathSegment>{
    let fields = filter_ignore_out(extract_fields(data));

    // let type_idents: Vec<&syn::Ident> = 
    fields.iter().map(|field| {
        if let syn::Field {
            ty: syn::Type::Path(
                syn::TypePath {
                    path: syn::Path {
                        ref segments,
                        ..
                    },
                    ..
                }
            ),
            ..
        } = field {
            &segments[0]
        } else {
            eprintln!("References are not supported with #[derive(MutConfig)]. Reference found within type.");
            unimplemented!()
        }
    }).collect::<Vec<&syn::PathSegment>>()
}

/// Extracts the generic arguments of types of fields from the struct.
/// 
/// If a type has no generic arguments, the vector is empty.
/// 
/// The first Vec layer represents the types. The second Vec layer represents the args.
/// 
/// To think about it better, it's like this:
/// 
/// ```
/// output.iter().map(|TYPE| {
///     TYPE.iter().map(|ARG| {
///         ...
///     })
/// })
/// ```
fn extract_generic_types(data: &Data) -> Vec<Vec<&syn::PathSegment>> {
    let types = extract_types(data);

    let something : Vec<Vec<&syn::PathSegment>> = types.iter().map(|ps| {
        let args = 
            if let syn::PathArguments::AngleBracketed(
                syn::AngleBracketedGenericArguments {
                    ref args,
                    ..
                }
            ) = &ps.arguments {
                Some(args)
            } else {
                None
            };

        let gentype : Vec<&syn::PathSegment> = args.map_or(vec![], |sa| {
            sa.iter().map(|a| {
                if let syn::GenericArgument::Type(syn::Type::Path(
                    syn::TypePath {
                        path : syn::Path {
                            ref segments,
                            ..
                        },
                        ..
                    }
                )) = a {
                    &segments[0]
                } else {
                    eprintln!("References are not supported with #[derive(MutConfig)]. Reference found within generic.");
                    unimplemented!()
                }
            }).collect()
        });

        gentype

    }).collect();

    something
}

/// Panics if the type name isn't compatible with the macro.
/// 
/// To be used by `derive` to avoid repetition.
fn incompatible_type_panic(tyname: &String) {
    panic!("Can't use \'{0}\' type - not yet supported by derive(MutConfig).\nHint: If you meant to add a struct implementing MutConfig, please name them in the following format: '{0}Config'\nPlease use one of the supported types as shown below:\n {1:#?}",tyname, ["isize", "String", "bool", "f64", "Vec<...>", "Option<...>"]);
}

/// Turns a field into an OVal. Uses the field name and its type.
fn into_oval(field: &Field, ty: &PathSegment) -> proc_macro2::TokenStream {
    let fname = &field.ident;
    let tname = &ty.ident;
    let tstr  = &tname.to_string();

    if tstr == "isize" {
        quote! {OInt(self.#fname.clone())}
    } else if tstr == "String" {
        quote! {OString(self.#fname.clone())}
    } else if tstr == "bool" {
        quote! {OBool(self.#fname.clone())}
    } else if tstr == "f64" {
        quote! {OFloat(self.#fname.clone())}
    } else if tstr == "Vec" {
        let gen = get_first_generic(&ty);
        // let v = into_oVal(&field, &gen);
        let v = into_subval(&gen, &String::from("a"));
        quote! {OArray(self.#fname.iter().map(|a| #v).collect())}
    } else if tstr == "Option" {
        let gen = get_first_generic(&ty);
        // let v = into_oVal(&field, &gen);
        let v = into_subval(&gen, &String::from("a"));
        quote! {self.#fname.clone().map_or(ONone(), |a| #v)}
    } else if tstr.find("Config").is_some() {
        quote! {OMap(self.#fname.to_hashmap())}
    } else {
        unimplemented!()
    }
}

/// Retrieves the first generic of a type.
/// 
/// For example: `Result<isize, usize>` -> `isize`.
fn get_first_generic(ty: &PathSegment) -> &PathSegment {
    let args =
        if let syn::PathArguments::AngleBracketed(
            syn::AngleBracketedGenericArguments {
                ref args,
                ..
            }
        ) = &ty.arguments {
            args
        } else {
            unimplemented!();
        };

    let typs : Vec<&PathSegment> = args.iter().map(|a| {
        if let syn::GenericArgument::Type(syn::Type::Path(
            syn::TypePath {
                path : syn::Path {
                    ref segments,
                    ..
                },
                ..
            }
        )) = a {
            &segments[0]
        } else {
            unimplemented!()
        }
    }).collect();

    typs[0]
}

/// Used by `into_oval` to represent nested OValues.
/// 
/// For example, in `Option<Vec<String>>`, `Option<...>` is initially parsed by
/// `into_oval`, however `Vec<...>` and `String` are parsed by this function.
/// 
/// In effect, any type A<B<...>> will have A<...> parsed in `into_oval`, and 
/// `B<...>` parsed in `into_subval`
fn into_subval(ty: &PathSegment, arg_name: &String) -> proc_macro2::TokenStream {
    let tname = &ty.ident;
    let tstr = &tname.to_string();

    let arg = syn::Ident::new(arg_name, tname.span());
    let new_arg = syn::Ident::new((arg_name.clone() + "a").as_str(), tname.span());

    if tstr == "isize" {
        quote! {OInt(#arg.clone())}
    } else if tstr == "String" {
        quote! {OString(#arg.clone())}
    } else if tstr == "bool" {
        quote! {OBool(#arg.clone())}
    } else if tstr == "f64" {
        quote! {OFloat(#arg.clone())}
    } else if tstr == "Vec" {
        let gen = get_first_generic(&ty);
        // let v = into_oVal(&field, &gen);
        let v = into_subval(&gen, &new_arg.to_string());
        quote! {OArray(#arg.iter().map(|#new_arg| #v).collect())}
    } else if tstr == "Option" {
        let gen = get_first_generic(&ty);
        // let v = into_oVal(&field, &gen);
        let v = into_subval(&gen, &new_arg.to_string());
        quote! {#arg.clone().map_or(ONone(), |#new_arg| #v)}
    } else if tstr.find("Config").is_some() {
        quote! {OMap(#arg.to_hashmap())}
    } else {
        incompatible_type_panic(&tstr);
        unimplemented!();
    }
}