rust-utils-macros 0.1.0

Procedural macros for the rust-utils crate
Documentation
use super::ok_or_compile_err;
use quote::{quote, ToTokens,};
use syn::{
    parse_quote,
    punctuated::Punctuated,
    spanned::Spanned,
    parse::Parser,
    Error as ParseError,
    Expr, Generics,
    Ident, Lit, Meta,
    Result as ParseResult,
    Token
};
use proc_macro2::TokenStream as TokenStream2;
use proc_macro::TokenStream;
use if_chain::if_chain;

// the type of config
#[derive(Copy, Clone, Default)]
enum ConfigType {
    #[default]
    Toml,
    Ron
}

#[derive(Clone, Default)]
struct ConfigData {
    file_name: String,
    save_dir: String,
    config_type: Option<ConfigType>
}

impl ConfigData {
    fn parse(input: TokenStream2) -> ParseResult<Self> {
        if input.is_empty() {
            Err(ParseError::new(input.span(), "Options \"file_name\" and \"save_dir\" are missing!"))
        }
        else {
            let mut data = Self::default();
            let options = <Punctuated<Meta, Token![,]>>::parse_terminated.parse2(input.clone())?;

            for option in options {
                match option {
                    Meta::List(meta_list) => {
                        let name = meta_list.path.require_ident()?;
                        let type_id = syn::parse2::<Ident>(meta_list.tokens.clone())?;

                        if name == "cfg_type"{
                            if type_id == "toml" {
                                data.config_type = Some(ConfigType::Toml);
                            }
                            else if type_id == "ron" {
                                data.config_type = Some(ConfigType::Ron);
                            }
                            else {
                                let msg = format!("Unknown config type {type_id}! Valid config type are \"toml\" and \"ron\"");
                                return Err(ParseError::new(input.span(), msg));
                            }
                        }
                        else {
                            let msg = format!("Unknown option {name}! Valid options are \"cfg_type\", \"file_name\", and \"save_dir\"");
                            return Err(ParseError::new(input.span(), msg));
                        }
                    }

                    Meta::NameValue(meta_val) => {
                        let name = meta_val.path.require_ident()?;

                        if_chain! {
                            if let Expr::Lit(literal) = &meta_val.value;
                            if let Lit::Str(str_val) = &literal.lit;

                            then {
                                if name == "file_name" {
                                    data.file_name = str_val.value();
                                }
                                else if name == "save_dir" {
                                    data.save_dir = str_val.value();
                                }
                            }
                            else {
                                let msg = format!("Unknown option {name}! Valid options are \"cfg_type\", \"file_name\", and \"save_dir\"");
                                return Err(ParseError::new(input.span(), msg));
                            }
                        }
                    }

                    _ => return Err(ParseError::new(input.span(), "Invalid Option!"))
                }
            }

            if data.file_name.is_empty() {
                Err(ParseError::new(input.span(), "Option \"file_name\" is missing!"))
            }
            else if data.save_dir.is_empty() {
                Err(ParseError::new(input.span(), "Option \"save_dir\" is missing!"))
            }
            else {
                Ok(data)
            }
        }
    }
}

pub fn gen_config_impl(type_def: &impl ToTokens, type_name: &Ident, generics: &Generics, attr_args: TokenStream) -> TokenStream2 {
    let cfg_data = ok_or_compile_err!(ConfigData::parse(attr_args.into()));
    let file_name = &cfg_data.file_name;
    let save_dir = &cfg_data.save_dir;
    let (impl_generics, ty_generics, where_clause) = generics.split_for_impl();

    let cfg_type_def: Option<TokenStream2> = if let Some(cfg_type) = cfg_data.config_type {
        let type_name: Ident = match cfg_type {
            ConfigType::Ron => parse_quote!(Ron),
            ConfigType::Toml => parse_quote!(Toml)
        };

        Some(quote!(const TYPE: rust_utils::config::ConfigType = rust_utils::config::ConfigType::#type_name;))
    }
    else { None };

    quote! {
        #[derive(serde::Deserialize, serde::Serialize)]
        #type_def

        impl #impl_generics rust_utils::config::Config for #type_name #ty_generics #where_clause {
            const FILE_NAME: &'static str = #file_name;
            #cfg_type_def

            fn get_save_dir() -> std::path::PathBuf {
                Self::get_config_root()
                    .join(#save_dir)
            }
        }
    }
}