crypt-macro 0.1.1

Macros for crypt-config
Documentation
use proc_macro::TokenStream;
use quote::quote;
use syn::{Data, DeriveInput, Error, Expr, Field, Fields, Meta, parse_macro_input};

fn get_field_attrs<'a>(field: &'a Field) -> (bool, Option<&'a Expr>) {
    let mut default_value = None;
    let mut is_optional = false;
    for attr in &field.attrs {
        match &attr.meta {
            Meta::NameValue(nv) => {
                let Some(attr_ident) = nv.path.get_ident() else {
                    continue;
                };

                if attr_ident != "default_value" {
                    continue;
                }

                default_value = Some(&nv.value);
                break;
            },
            Meta::Path(path) => {
                let Some(ident) = path.get_ident() else {
                    continue;
                };

                if ident == "optional" {
                    is_optional = true;
                    break;
                }
            }
            _ => ()
        }
    }

    (is_optional, default_value)
}

#[proc_macro_derive(Cryptic, attributes(default_value, optional))]
/// Allows structs and enums to automatically be constructed from a
/// parsed crypt file.
///
/// Properties can additionally be given default values (via `default_value`) or
/// automatically be set to [None] when not found (via `optional`).
pub fn cryptic_derived(input: TokenStream) -> TokenStream {
    let input = parse_macro_input!(input as DeriveInput);
    let obj_name = input.ident;

    let implementation = match &input.data {
        Data::Struct(data_struct) => {
            match &data_struct.fields {
                Fields::Named(named_fields) => {
                    let field_construction = named_fields.named.iter().map(|f| {
                        let (is_optional, default_value) = get_field_attrs(f);
                        let ident = f.ident.as_ref().unwrap();
                        match default_value {
                            Some(def) => {
                                quote! {
                                    #ident: mapping
                                        .remove(stringify!(#ident))
                                        .map(|i|
                                            ::crypt_configs::Cryptic::cryptic(i))
                                            .unwrap_or(Ok(match #def.parse() { Ok(v) => v, Err(_) => panic!("Failed to parse the default value for {}", stringify!(#ident)) })
                                        )?,
                                }
                            }

                            None => {
                                if is_optional {
                                    quote! {
                                        #ident: match mapping.remove(stringify!(#ident)) {
                                            Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                            None => None,
                                        },
                                    }
                                } else {
                                    quote! {
                                        #ident: match mapping.remove(stringify!(#ident)) {
                                            Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                            None => return Err(::crypt_configs::CryptError::cannot_find_ident(stringify!(#ident))),
                                        },
                                    }
                                }
                            }
                        }
                    });

                    quote! {
                        let mut mapping: ::std::collections::HashMap<String, ::crypt_configs::TracedObject> = ::crypt_configs::Cryptic::cryptic(object)?;
                        Ok(Self {
                            #(#field_construction)*
                        })
                    }
                },

                Fields::Unnamed(unnammed_fields) => {
                    let field_construction = unnammed_fields.unnamed.iter().map(|f| {
                        let (is_optional, default_value) = get_field_attrs(f);
                        match default_value {
                            Some(def) => {
                                quote! {
                                    match it.next() {
                                        Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                        None => match #def.parse() { Ok(v) => v, Err(_) => panic!("Failed to parse default value") }
                                    },
                                }
                            }

                            None => {
                                if is_optional {
                                    quote! {
                                        match it.next() {
                                            Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                            None => None,
                                        },
                                    }
                                } else {
                                    quote! {
                                        match it.next() {
                                            Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                            None => return Err(::crypt_configs::CryptError::not_enough_items())
                                        },
                                    }
                                }
                            }
                        }
                    });

                    quote! {
                        let items: Vec<::crypt_configs::TracedObject> = Cryptic::cryptic(object)?;
                        let mut it = items.into_iter();

                        Ok(Self(
                            #(#field_construction)*
                        ))
                    }
                },

                Fields::Unit =>
                    return TokenStream::from(Error::new(obj_name.span(), "Struct must have at least one attribute").to_compile_error())
            }
        },

        Data::Enum(data_enum) => {
            let variant_bodies = data_enum.variants.iter().map(|var| {
                let var_ident = &var.ident;
                match &var.fields {
                    Fields::Named(named_fields) => {
                        let field_construction = named_fields.named.iter().map(|f| {
                            let (is_optional, default_value) = get_field_attrs(f);
                            let ident = f.ident.as_ref().unwrap();
                            match default_value {
                                Some(def) => {
                                    quote! {
                                        #ident: mapping
                                            .remove(stringify!(#ident))
                                            .map(|i|
                                                ::crypt_configs::Cryptic::cryptic(i))
                                                .unwrap_or(Ok(match #def.parse() { Ok(v) => v, Err(_) => panic!("Failed to parse the default value for {}", stringify!(#ident)) })
                                            )?,
                                    }
                                }

                                None => {
                                    if is_optional {
                                        quote! {
                                            #ident: match mapping.remove(stringify!(#ident)) {
                                                Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                                None => None,
                                            },
                                        }
                                    } else {
                                        quote! {
                                            #ident: match mapping.remove(stringify!(#ident)) {
                                                Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                                None => return Err(::crypt_configs::CryptError::cannot_find_ident(stringify!(#ident))),
                                            },
                                        }
                                    }
                                }
                            }
                        });

                        quote! {
                            {
                                let mut mapping: ::std::collections::HashMap<String, ::crypt_configs::TracedObject> = ::crypt_configs::Cryptic::cryptic(object)?;
                                Ok(Self::#var_ident {
                                    #(#field_construction)*
                                })
                            }
                        }
                    },

                    Fields::Unnamed(unnammed_fields) => {
                        let field_construction = unnammed_fields.unnamed.iter().map(|f| {
                            let (is_optional, default_value) = get_field_attrs(f);
                            match default_value {
                                Some(def) => {
                                    quote! {
                                        match it.next() {
                                            Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                            None => match #def.parse() { Ok(v) => v, Err(_) => panic!("Failed to parse default value") }
                                        },
                                    }
                                }

                                None => {
                                    if is_optional {
                                        quote! {
                                            match it.next() {
                                                Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                                None => None,
                                            },
                                        }
                                    } else {
                                        quote! {
                                            match it.next() {
                                                Some(v) => ::crypt_configs::Cryptic::cryptic(v)?,
                                                None => return Err(::crypt_configs::CryptError::not_enough_items())
                                            },
                                        }
                                    }
                                }
                            }
                        });

                        quote! {
                            {
                                let items: Vec<::crypt_configs::TracedObject> = Cryptic::cryptic(object)?;
                                let mut it = items.into_iter();

                                Ok(Self::#var_ident(
                                    #(#field_construction)*
                                ))
                            }
                        }
                    },

                    Fields::Unit => quote! {
                        Ok(Self::#var_ident),
                    }
                }
            });

            let variant_patterns = data_enum.variants.iter().map(|var| {
                let var_ident = &var.ident;
                quote! {stringify!(#var_ident)}
            });

            quote! {
                let obj: ::crypt_configs::Tagged<String, ::crypt_configs::TracedObject> = ::crypt_configs::Cryptic::cryptic(object)?;
                let object = obj.object;
                match obj.tag.as_str() {
                    #(
                        #variant_patterns => #variant_bodies
                    )*

                    _ => Err(::crypt_configs::CryptError::invalid_enum_variant(obj.tag))
                }
            }
        },

        Data::Union(_) => {
            return TokenStream::from(Error::new(obj_name.span(), "Only structs and enums can derive Cryptic").to_compile_error())
        }
    };

    TokenStream::from(quote! {
        impl ::crypt_configs::Cryptic for #obj_name {
            fn cryptic(object: ::crypt_configs::TracedObject) -> Result<Self, ::crypt_configs::CryptError> {
                #implementation
            }
        }
    })
}