Skip to main content

fondant_derive/
lib.rs

1//! Please refer to the `fondant` crate for usage instructions
2
3extern crate proc_macro;
4
5use ::std::ffi::{OsStr, OsString};
6use ::std::path::Path;
7use proc_macro::TokenStream;
8use proc_macro2::Span;
9use quote::quote;
10use syn::{parse_macro_input, DeriveInput, Ident, Lit, Meta, MetaNameValue};
11
12#[derive(Debug, Default)]
13struct ConfigPath {
14    parent: String,
15    filename: Option<OsString>,
16    extension: Option<OsString>,
17}
18
19#[proc_macro_derive(Configure, attributes(config_file))]
20pub fn config_attribute(item: TokenStream) -> TokenStream {
21    let ast: DeriveInput = parse_macro_input!(item as DeriveInput);
22    let cfg_path = extract_attributes(&ast);
23
24    gen_impl(&ast, cfg_path)
25}
26
27fn extract_attributes(ast: &DeriveInput) -> ConfigPath {
28    for option in ast.attrs.iter() {
29        let option = option.parse_meta().unwrap();
30        match option {
31            Meta::NameValue(MetaNameValue {
32                ref path, ref lit, ..
33            }) if path.is_ident("config_file") => {
34                if let Lit::Str(f) = lit {
35                    let f = f.value();
36                    let fp = Path::new(&f);
37                    let parent = fp.parent().unwrap_or(Path::new(""));
38                    return ConfigPath {
39                        parent: parent.to_str().unwrap().into(),
40                        filename: fp.file_stem().map(OsStr::to_os_string),
41                        extension: fp.extension().map(OsStr::to_os_string),
42                    };
43                }
44            }
45            _ => {}
46        }
47    }
48    return Default::default();
49}
50
51fn pick_serializer(ext: &str) -> (Ident, Ident) {
52    /* returns serializer and a corresponding function to
53     * stringify with based on file extension
54     * toml::to_string_pretty
55     * serde_yaml::to_string
56     * serde_json::to_string_pretty
57     */
58    match ext.as_ref() {
59        "toml" => (
60            Ident::new("toml", Span::call_site()),
61            Ident::new("to_string_pretty", Span::call_site()),
62        ),
63        "yaml" => (
64            Ident::new("serde_yaml", Span::call_site()),
65            Ident::new("to_string", Span::call_site()),
66        ),
67        "json" => (
68            Ident::new("serde_json", Span::call_site()),
69            Ident::new("to_string_pretty", Span::call_site()),
70        ),
71        _ => panic!("Invalid extension!"),
72    }
73}
74
75fn gen_impl(ast: &DeriveInput, cfg_path: ConfigPath) -> TokenStream {
76    let struct_ident = &ast.ident;
77
78    let filename = cfg_path
79        .filename
80        .unwrap_or(OsStr::new("config").to_os_string())
81        .into_string()
82        .unwrap();
83
84    let filetype = cfg_path
85        .extension
86        .unwrap_or(OsStr::new("toml").to_os_string())
87        .into_string()
88        .unwrap();
89
90    let parent = cfg_path.parent;
91
92    let (ser, ser_fn) = pick_serializer(&filetype);
93
94    let includes = quote! {
95        use ::fondant::fondant_exports::*;
96        use ::fondant::FondantError ;
97        use ::std::option::Option;
98        use ::std::fs::{self, File, OpenOptions};
99        use ::std::io::prelude::*;
100        use ::std::io::{ ErrorKind::NotFound, Write };
101        use ::std::ffi::{OsStr, OsString};
102        use ::std::path::{Path, PathBuf};
103    };
104
105    let load_paths = quote! {
106        let pkg_name = env!("CARGO_PKG_NAME");
107        let project = ProjectDirs::from("rs", "", pkg_name).unwrap();
108        let default_dir: String = project.config_dir().to_str().unwrap().into();
109
110        let d = if #parent != "" { #parent.into() } else { default_dir };
111        let config_dir: String = expand_tilde(d)
112            .as_path()
113            .to_str()
114            .unwrap()
115            .into();
116
117        let tip = Path::new(&#filename).with_extension(&#filetype);
118        let mut config_file = PathBuf::from(&config_dir);
119        config_file.push(tip);
120    };
121
122    let gen = quote! {
123        #includes
124        impl Configure for #struct_ident {
125            fn load() -> Result<#struct_ident, FondantError> {
126                #load_paths
127                match File::open(&config_file) {
128                    Ok(mut cfg) => {
129                        let mut cfg_data = String::new();
130                        cfg.read_to_string(&mut cfg_data).unwrap();
131
132                        let config: #struct_ident = #ser::from_str(&cfg_data[..])
133                            .map_err(|_| FondantError::ConfigParseError)?;
134                        return Ok(config);
135                    },
136                    Err(ref e) if e.kind() == NotFound => {
137                        if !Path::new(&config_dir).is_dir() {
138                            fs::create_dir_all(config_dir).map_err(FondantError::DirCreateErr)?;
139                        }
140                        let default_impl = #struct_ident::default();
141                        Configure::store(&default_impl)?;
142                        return Ok(default_impl);
143                    },
144                    Err(e) => return Err(FondantError::LoadError),
145                };
146            }
147            fn store(&self) -> Result<(), FondantError> {
148                #load_paths
149                let mut f = OpenOptions::new()
150                    .write(true)
151                    .create(true)
152                    .truncate(true)
153                    .open(config_file)
154                    .map_err(|_| FondantError::FileOpenError)?;
155
156                let s = #ser::#ser_fn(self).map_err(|_| FondantError::ConfigParseError)?;
157                f.write_all(s.as_bytes()).map_err(|_| FondantError::FileWriteError)?;
158                Ok(())
159            }
160        }
161    };
162    gen.into()
163}