livemod-derive 0.3.0

Derive macro for livemod
Documentation
use proc_macro2::{Ident, TokenStream};
use quote::quote;
use syn::{parenthesized, parse::Parse, punctuated::Punctuated, DeriveInput, LitStr, Token};

#[proc_macro_derive(LiveMod, attributes(livemod))]
pub fn livemod_derive(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast: DeriveInput = syn::parse(input).unwrap();

    match ast.data {
        syn::Data::Struct(st) => match st.fields {
            syn::Fields::Named(fields) => {
                let struct_name = ast.ident;
                let (fields, matches_gets) = fields
                    .named
                    .into_iter()
                    .filter_map(|field| {
                        let attrs = match field
                            .attrs
                            .into_iter()
                            .filter_map(|attr| {
                                if attr.path.is_ident("livemod") {
                                    Some(syn::parse2(attr.tokens))
                                } else {
                                    None
                                }
                            })
                            .collect::<Result<Vec<_>, _>>()
                        {
                            Ok(attrs) => attrs,
                            Err(error) => {
                                let ident = field.ident.unwrap();
                                let name = ident.to_string();
                                return Some((
                                    error.to_compile_error(),
                                    (quote! { #name => &mut self.#ident }, quote! { (#name.to_owned(), ::livemod::LiveMod::get_self(&self.#ident)) }),
                                ));
                            }
                        };
                        if !attrs.iter().any(|attr| matches!(attr, Attr::Skip)) {
                            let ident = field.ident.unwrap();
                            let name = attrs
                                .iter()
                                .filter_map(|attr| match attr {
                                    Attr::Rename(name) => Some(name.clone()),
                                    _ => None,
                                })
                                .next_back()
                                .unwrap_or({
                                    let mut name = ident.to_string();
                                    name.as_mut_str()[..1].make_ascii_uppercase();
                                    name = name.replace('_', " ");
                                    name
                                });
                            let repr = if let Some(Attr::Repr(trait_, args)) =
                                attrs.iter().rfind(|attr| matches!(attr, Attr::Repr(_, _)))
                            {
                                let mut repr_method = format!("repr_{}", trait_,);
                                repr_method.make_ascii_lowercase();
                                let repr_method = Ident::new(&repr_method, trait_.span());
                                quote! { #trait_::#repr_method(&self.#ident, #args) }
                            } else {
                                quote! { ::livemod::LiveMod::repr_default(&self.#ident) }
                            };
                            Some((
                                quote! {
                                    ::livemod::TrackedData {
                                        name: String::from(#name),
                                        data_type: #repr,
                                        triggers: vec![],
                                    }
                                },
                                (
                                    quote! {
                                        #name => &mut self.#ident
                                    },
                                    quote! {
                                        (#name.to_owned(), ::livemod::LiveMod::get_self(&self.#ident))
                                    }
                                )
                            ))
                        } else {
                            None
                        }
                    })
                    .unzip::<_, _, Vec<_>, Vec<_>>();

                let (matches, gets) = matches_gets.into_iter().unzip::<_, _, Vec<_>, Vec<_>>();
                let gen = quote! {
                    #[automatically_derived]
                    impl ::livemod::LiveMod for #struct_name {
                        fn repr_default(&self) -> ::livemod::TrackedDataRepr {
                            ::livemod::TrackedDataRepr::Struct {
                                name: String::from(stringify!(#struct_name)),
                                fields: vec![
                                    #(#fields),*
                                ],
                                triggers: vec![]
                            }
                        }

                        fn get_named_value(&mut self, name: &str) -> &mut ::livemod::LiveMod {
                            match name {
                                #(#matches ,)*
                                _ => panic!("Unexpected value name!"),
                            }
                        }

                        fn trigger(&mut self, trigger: ::livemod::Trigger) -> bool {
                            panic!("Unexpected trigger operation!")
                        }

                        fn get_self(&self) -> ::livemod::TrackedDataValue {
                            ::livemod::TrackedDataValue::Struct(vec![
                                #(#gets),*
                            ])
                        }
                    }
                };
                gen.into()
            }
            syn::Fields::Unnamed(_fields) => todo!(),
            syn::Fields::Unit => todo!(),
        },
        syn::Data::Enum(_en) => todo!(),
        syn::Data::Union(_) => todo!(),
    }
}

enum Attr {
    Skip,
    Rename(String),
    Repr(Ident, Punctuated<TokenStream, Token![,]>),
}

impl Parse for Attr {
    fn parse(direct_input: syn::parse::ParseStream) -> syn::Result<Self> {
        let input;
        parenthesized!(input in direct_input);
        let attr_type: Ident = input.parse()?;
        if attr_type == "skip" {
            if !input.is_empty() {
                Err(input.error("Expected end of attribute content"))?;
            }
            Ok(Attr::Skip)
        } else if attr_type == "rename" {
            input.parse::<Token![=]>()?;
            let new_name: LitStr = input.parse()?;
            Ok(Attr::Rename(new_name.value()))
        } else if attr_type == "repr" {
            input.parse::<Token![=]>()?;
            let trait_name = input.parse()?;
            if !input.is_empty() {
                let arguments;
                parenthesized!(arguments in input);
                Ok(Attr::Repr(
                    trait_name,
                    arguments.parse_terminated(TokenStream::parse)?,
                ))
            } else {
                Ok(Attr::Repr(trait_name, Punctuated::new()))
            }
        } else {
            Err(syn::Error::new(
                attr_type.span(),
                "Unrecognised attribute tag",
            ))
        }
    }
}