macroex-derive 0.2.0

Derive macros for the macroex crate.
Documentation
use proc_macro2::{TokenStream, Ident, TokenTree, Span};
use proc_macro_error::abort;
use syn::{DeriveInput, Generics, Attribute, FieldsNamed};
use quote::{quote, format_ident};

fn to_converter(case: &str, span: Span) -> fn(String) -> String {
    use convert_case::Case;
    match case {
        "lowercase" => |x| convert_case::Casing::to_case(&x, Case::Flat),
        "UPPERCASE" => |x| convert_case::Casing::to_case(&x, Case::UpperFlat),
        "PascalCase" => |x| convert_case::Casing::to_case(&x, Case::Pascal),
        "camelCase" => |x| convert_case::Casing::to_case(&x, Case::Camel),
        "snake_case" => |x| convert_case::Casing::to_case(&x, Case::Snake),
        "SCREAMING_SNAKE_CASE" => |x| convert_case::Casing::to_case(&x, Case::UpperSnake),
        "kebab-case" => |x| convert_case::Casing::to_case(&x, Case::Kebab),
        "SCREAMING-KEBAB-CASE" => |x| convert_case::Casing::to_case(&x, Case::UpperKebab),
        _ => abort!(span, "Invalid caseing, see https://serde.rs/container-attrs.html#rename_all for a list of supported casings.")
    }
}

fn parse_main_attr(attr: TokenStream, rename: &mut fn(String) -> String) {
    let mut iter = attr.into_iter();
    loop {
        match iter.next() {
            Some(TokenTree::Ident(ident)) => {
                match ident.to_string().as_str() {
                    "rename_all" => {
                        match iter.next() {
                            Some(TokenTree::Punct(p)) if p.as_char() == '=' => (),
                            Some(tt) => abort!(tt.span(), r#"Expected `=`"#),
                            None => abort!(ident.span(), r#"Expected `=`"#),
                        };
                        match iter.next() {
                            Some(tt) => {
                                if let Ok(str) = litrs::StringLit::try_from(&tt) {
                                    *rename = to_converter(str.value(), tt.span()) 
                                } else {
                                    abort!(tt.span(), "Expected string literal.")
                                }
                            }
                            None => abort!(ident.span(), "Expected string literal."),
                        }
                    },
                    _ => abort!(ident.span(), r#"Expected one of "rename" or "rename_all"."#)
                }
            }
            None => return,
            Some(tt) => abort!(tt.span(), r#"Expected one of "rename" or "rename_all"."#)
        }
    }
}


fn parse_attrs(attr: TokenStream, default: &mut Option<TokenStream>, repeat: &mut bool, required: &mut bool, rename: &mut Option<String>) {
    let mut iter = attr.into_iter();
    loop {
        match iter.next() {
            Some(TokenTree::Ident(ident)) => {
                match ident.to_string().as_str() {
                    "required" => *required = true,
                    "repeat" => *repeat = true,
                    "default" => {
                        match iter.next() {
                            Some(TokenTree::Punct(p)) if p.as_char() == '=' => (),
                            Some(tt) => abort!(tt.span(), r#"Expected `=`"#),
                            None => abort!(ident.span(), r#"Expected `=`"#),
                        };
                        match iter.next() {
                            Some(tt) => {
                                if let Ok(str) = litrs::StringLit::try_from(&tt) {
                                    *default = match syn::parse_str(&str.value()) {
                                        Ok(v) => Some(v),
                                        Err(err) => abort!(tt.span(), "{}", err),
                                    }
                                } else {
                                    abort!(tt.span(), "Expected string literal.")
                                }
                            }
                            None => abort!(ident.span(), "Expected string literal."),
                        }
                    },
                    "rename" => {
                        match iter.next() {
                            Some(TokenTree::Punct(p)) if p.as_char() == '=' => (),
                            Some(tt) => abort!(tt.span(), r#"Expected `=`"#),
                            None => abort!(ident.span(), r#"Expected `=`"#),
                        };
                        match iter.next() {
                            Some(tt) => {
                                if let Ok(str) = litrs::StringLit::try_from(&tt) {
                                    *rename = Some(str.into_value().into_owned()) 
                                } else {
                                    abort!(tt.span(), "Expected string literal.")
                                }
                            }
                            None => abort!(ident.span(), "Expected string literal."),
                        }
                    },
                    _ => abort!(ident.span(), r#"Expected one of "required", "repeat", "default" or "rename"."#)
                };
                match iter.next() {
                    Some(TokenTree::Punct(p)) if p.as_char() == ',' => (),
                    None => return,
                    Some(tt) => abort!(tt.span(), r#"Expected `,`"#)
                }
            }
            None => return,
            Some(tt) => abort!(tt.span(), r#"Expected one of "required", "repeat", "default" or "rename"."#)
        }
    }
} 

pub fn attr_extract<'t> (attrs: &'t [Attribute]) -> impl IntoIterator<Item = TokenStream> + 't{
    attrs.into_iter().filter_map(|x| match &x.meta {
        syn::Meta::List(list) => {
            if list.path.is_ident(&format_ident!("macroex")) {
                Some(list.tokens.clone())
            } else {
                None
            }
        },
        _ => None,
    })
}

pub fn from_struct(fields: FieldsNamed, global_rename: fn(String) -> String) -> Vec<TokenStream> {
    let mut result_fields = Vec::new();
        
    for field in fields.named {
        let ident = field.ident.unwrap();
        let ty = field.ty;
        let mut default = None;
        let (mut repeat, mut required) = (false, false);
        let mut rename = None;

        for attr in attr_extract(&field.attrs){ 
            parse_attrs(attr, &mut default, &mut repeat, &mut required, &mut rename);
        }

        let name = rename.unwrap_or_else(
            ||global_rename(ident.to_string())
        );

        let field_row = if repeat {
            quote!(
                #ident: map.remove_all(#name).into_iter()
                    .map(|x| <#ty as ::std::iter::IntoIterator>::Item::from_tokens(x))
                    .collect::<Result<_,_>>()?
            )
        } else if let Some(default) = default{
            quote!(#ident: match map.remove(#name) {
                Some(x) => <#ty>::from_tokens(x)?,
                None => #default,
            })
        } else if required{
            quote!(#ident: <#ty>::from_tokens(match map.remove(#name){
                Some(value) => value,
                None => bail!(span, "Required field `{}` not found.", #name),
            })?)
        } else {
            quote!(#ident: match map.remove(#name) {
                Some(x) => <#ty>::from_tokens(x)?,
                None => ::std::default::Default::default(),
            })
        };
        result_fields.push(field_row);
    }
    result_fields
}

pub fn format_struct(name: Ident, generics: Generics, fields: Vec<TokenStream>) -> TokenStream{
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();
    quote! {
#[automatically_derived]
const _: () = {
    use ::macroex::{FromMacro, MetaMap, IdentString, EitherStream, NamedStructExtractor, Parenthesized, bail};
    use ::macroex::proc_macro2::{TokenTree, TokenStream, Span};
    impl #impl_generics FromMacro for #name #ty_generics #where_clause {
        fn from_one(tt: TokenTree) -> Result<Self, macroex::Error> {
            let Parenthesized(span) = Parenthesized::from_one(tt)?;
            Self::from_many(span)
        }
        fn from_many(tokens: proc_macro2::TokenStream) -> Result<Self, macroex::Error> {
            let span = Span::call_site();
            let mut map = MetaMap::from_many(tokens)?;
            Ok(Self {#(#fields),*})
        }
    }
};
    }
}

pub fn from_meta(tokens: TokenStream) -> TokenStream {
    let ast: DeriveInput = syn::parse2(tokens).unwrap();
    let name = ast.ident;
    match ast.data {
        syn::Data::Union(union) => abort!(union.union_token.span, "Only named struct is allowed."),
        syn::Data::Enum(e) => abort!(e.enum_token.span, "Only named struct is allowed."),
        syn::Data::Struct(s) => {
            match s.fields {
                syn::Fields::Unit => {
                    abort!(s.struct_token.span, "Only named struct is allowed.");
                },
                syn::Fields::Unnamed(_) => {
                    abort!(s.struct_token.span, "Only named struct is allowed.");
                }
                syn::Fields::Named(fields) => {
                    let mut rename: fn(String) -> String = |s| s;
                    for attr in attr_extract(&ast.attrs) {
                        parse_main_attr(attr, &mut rename);
                    };
                    let fields = from_struct(fields, rename);
                    format_struct(name, ast.generics, fields)
                }
            }
        }
    }
}