From a4e98bf35f1d1e55c6471aba51851f122e018370 Mon Sep 17 00:00:00 2001 From: Akshay Date: Sun, 15 Mar 2020 21:54:52 +0530 Subject: add derive macros --- confondant_derive/.gitignore | 10 ++++ confondant_derive/Cargo.toml | 16 ++++++ confondant_derive/src/lib.rs | 125 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 151 insertions(+) create mode 100644 confondant_derive/.gitignore create mode 100644 confondant_derive/Cargo.toml create mode 100644 confondant_derive/src/lib.rs diff --git a/confondant_derive/.gitignore b/confondant_derive/.gitignore new file mode 100644 index 0000000..088ba6b --- /dev/null +++ b/confondant_derive/.gitignore @@ -0,0 +1,10 @@ +# Generated by Cargo +# will have compiled files and executables +/target/ + +# Remove Cargo.lock from gitignore if creating an executable, leave it for libraries +# More information here https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html +Cargo.lock + +# These are backup files generated by rustfmt +**/*.rs.bk diff --git a/confondant_derive/Cargo.toml b/confondant_derive/Cargo.toml new file mode 100644 index 0000000..4b94b57 --- /dev/null +++ b/confondant_derive/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "confondant_derive" +version = "0.1.0" +authors = ["Akshay "] +edition = "2018" + +[dependencies] +proc-macro2 = "1.0.9" +quote = "1.0" + +[dependencies.syn] +version = "1.0" +features = ["full"] + +[lib] +proc-macro = true diff --git a/confondant_derive/src/lib.rs b/confondant_derive/src/lib.rs new file mode 100644 index 0000000..076a66e --- /dev/null +++ b/confondant_derive/src/lib.rs @@ -0,0 +1,125 @@ +extern crate proc_macro; + +use proc_macro::TokenStream; +use proc_macro2::Span; +use quote::quote; +use syn::{parse_macro_input, DeriveInput, Error, Ident, Lit, Meta, MetaNameValue, Result}; + +#[proc_macro_derive(Config, attributes(filename, extension))] +pub fn config_attribute(item: TokenStream) -> TokenStream { + let ast: DeriveInput = parse_macro_input!(item as DeriveInput); + let (filename, filetype) = extract_attributes(&ast); + + gen_impl( + &ast, + filename.unwrap_or("config".into()), + filetype.unwrap_or("toml".into()), + ) +} + +fn extract_attributes(ast: &DeriveInput) -> (Option, Option) { + let mut filename: Option = None; + let mut filetype: Option = None; + for option in ast.attrs.iter() { + let option = option.parse_meta().unwrap(); + match option { + Meta::NameValue(MetaNameValue { + ref path, ref lit, .. + }) if path.is_ident("filename") => { + if let Lit::Str(f) = lit { + filename = Some(f.value()); + } + } + Meta::NameValue(MetaNameValue { + ref path, ref lit, .. + }) if path.is_ident("extension") => { + if let Lit::Str(f) = lit { + filetype = Some(f.value()); + } + } + _ => {} + } + } + return (filename, filetype); +} + +fn map_ser(ext: &str) -> (Ident, Ident) { + match ext.as_ref() { + "toml" => ( + Ident::new("toml", Span::call_site()), + Ident::new("to_string_pretty", Span::call_site()), + ), + "yaml" => ( + Ident::new("serde_yaml", Span::call_site()), + Ident::new("to_string", Span::call_site()), + ), + "json" => ( + Ident::new("serde_json", Span::call_site()), + Ident::new("to_string_pretty", Span::call_site()), + ), + _ => panic!("Invalid extension!"), + } +} + +fn gen_impl(ast: &DeriveInput, filename: String, filetype: String) -> TokenStream { + let struct_name = &ast.ident.to_string(); + let struct_ident = &ast.ident; + let (ser, ser_fn) = map_ser(&filetype); + + let gen = quote! { + use ::confondant::{ ProjectDirs, toml, serde_json, serde_yaml, FondantError }; + use ::std::path::{ Path, PathBuf }; + use ::std::option::Option; + use ::std::fs::{self, File, OpenOptions}; + use ::std::io::prelude::*; + use ::std::io::{ ErrorKind::NotFound, Write }; + + impl Config for #struct_ident { + fn load() -> Result<#struct_ident, FondantError> { + let project = ProjectDirs::from("rs", "", #struct_name).ok_or(FondantError::InvalidHomeDir)?; + let config_dir = project.config_dir(); + + let tip = (Path::new(#filename)).with_extension(&#filetype); + let config_file: PathBuf = [config_dir, &tip].iter().collect(); + + match File::open(&config_file) { + Ok(mut cfg) => { + // the file exists, parse the toml and return the struct + let mut cfg_data = String::new(); + cfg.read_to_string(&mut cfg_data); + + let config: #struct_ident = #ser::from_str(&cfg_data[..]) + .map_err(|_| FondantError::ConfigParseError)?; + return Ok(config); + }, + Err(ref e) if e.kind() == NotFound => { + fs::create_dir_all(project.config_dir()); + let default_impl = #struct_ident::default(); + Config::store(&default_impl)?; + return Ok(default_impl); + }, + Err(e) => return Err(FondantError::LoadError), + }; + } + fn store(&self) -> Result<(), FondantError> { + let project = ProjectDirs::from("rs", "", #struct_name).ok_or(FondantError::InvalidHomeDir)?; + let config_dir = project.config_dir(); + + let tip = (Path::new(#filename)).with_extension(&#filetype); + let config_file: PathBuf = [config_dir, &tip].iter().collect(); + + let mut f = OpenOptions::new() + .write(true) + .create(true) + .truncate(true) + .open(config_file) + .map_err(|_| FondantError::FileOpenError)?; + + let s = #ser::#ser_fn(self).map_err(|_| FondantError::ConfigParseError)?; + f.write_all(s.as_bytes()).map_err(|_| FondantError::FileWriteError); + Ok(()) + } + } + }; + gen.into() +} -- cgit v1.2.3