aboutsummaryrefslogtreecommitdiff
path: root/confondant_derive/src/lib.rs
blob: 076a66ecb1c9e1912eae69b962e53cc8c185871e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
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<String>, Option<String>) {
    let mut filename: Option<String> = None;
    let mut filetype: Option<String> = 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()
}