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