cosmic_config_derive/
lib.rs

1use proc_macro::TokenStream;
2use quote::quote;
3use syn::{self};
4
5#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))]
6pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream {
7    // Construct a representation of Rust code as a syntax tree
8    // that we can manipulate
9    let ast = syn::parse(input).unwrap();
10
11    // Build the trait implementation
12    impl_cosmic_config_entry_macro(&ast)
13}
14
15fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream {
16    let attributes = &ast.attrs;
17    let version = attributes
18        .iter()
19        .find_map(|attr| {
20            if attr.path.is_ident("version") {
21                match attr.parse_meta() {
22                    Ok(syn::Meta::NameValue(syn::MetaNameValue {
23                        lit: syn::Lit::Int(lit_int),
24                        ..
25                    })) => Some(lit_int.base10_parse::<u64>().unwrap()),
26                    _ => None,
27                }
28            } else {
29                None
30            }
31        })
32        .unwrap_or(0);
33
34    let name = &ast.ident;
35
36    // Get the fields of the struct
37    let fields = match ast.data {
38        syn::Data::Struct(ref data_struct) => match data_struct.fields {
39            syn::Fields::Named(ref fields) => &fields.named,
40            _ => unimplemented!("Only named fields are supported"),
41        },
42        _ => unimplemented!("Only structs are supported"),
43    };
44
45    let write_each_config_field = fields.iter().map(|field| {
46        let field_name = &field.ident;
47        quote! {
48            cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?;
49        }
50    });
51
52    let get_each_config_field = fields.iter().map(|field| {
53        let field_name = &field.ident;
54        let field_type = &field.ty;
55        quote! {
56            match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) {
57                Ok(#field_name) => default.#field_name = #field_name,
58                Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (),
59                Err(e) => errors.push(e),
60            }
61        }
62    });
63
64    let update_each_config_field = fields.iter().map(|field| {
65        let field_name = &field.ident;
66        let field_type = &field.ty;
67        quote! {
68            stringify!(#field_name) => {
69                match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) {
70                    Ok(value) => {
71                        if self.#field_name != value {
72                            keys.push(stringify!(#field_name));
73                        }
74                        self.#field_name = value;
75                    },
76                    Err(e) => {
77                        errors.push(e);
78                    }
79                }
80            }
81        }
82    });
83
84    let setters = fields.iter().filter_map(|field| {
85        let field_name = &field.ident.as_ref()?;
86        let field_type = &field.ty;
87        let setter_name = quote::format_ident!("set_{}", field_name);
88        let doc = format!("Sets [`{name}::{field_name}`] and writes to [`cosmic_config::Config`] if changed");
89        Some(quote! {
90            #[doc = #doc]
91            ///
92            /// Returns `Ok(true)` when the field's value has changed and was written to disk
93            pub fn #setter_name(&mut self, config: &cosmic_config::Config, value: #field_type) -> Result<bool, cosmic_config::Error> {
94                if self.#field_name != value {
95                    self.#field_name = value;
96                    cosmic_config::ConfigSet::set(config, stringify!(#field_name), &self.#field_name)?;
97                    Ok(true)
98                } else {
99                    Ok(false)
100                }
101            }
102        })
103    });
104
105    let gen = quote! {
106        impl CosmicConfigEntry for #name {
107            const VERSION: u64 = #version;
108
109            fn write_entry(&self, config: &cosmic_config::Config) -> Result<(), cosmic_config::Error> {
110                let tx = config.transaction();
111                #(#write_each_config_field)*
112                tx.commit()
113            }
114
115            fn get_entry(config: &cosmic_config::Config) -> Result<Self, (Vec<cosmic_config::Error>, Self)> {
116                let mut default = Self::default();
117                let mut errors = Vec::new();
118
119                #(#get_each_config_field)*
120
121                if errors.is_empty() {
122                    Ok(default)
123                } else {
124                    Err((errors, default))
125                }
126            }
127
128            fn update_keys<T: AsRef<str>>(&mut self, config: &cosmic_config::Config, changed_keys: &[T]) -> (Vec<cosmic_config::Error>, Vec<&'static str>){
129                let mut keys = Vec::with_capacity(changed_keys.len());
130                let mut errors = Vec::new();
131                for key in changed_keys.iter() {
132                    match key.as_ref() {
133                        #(#update_each_config_field)*
134                        _ => (),
135                    }
136                }
137                (errors, keys)
138            }
139        }
140
141        impl #name {
142            #(#setters)*
143        }
144    };
145
146    gen.into()
147}