gsettings_macro/
lib.rs

1#![warn(rust_2018_idioms)]
2#![deny(rustdoc::broken_intra_doc_links)]
3#![doc = include_str!("../README.md")]
4
5mod generators;
6mod schema;
7
8use deluxe::SpannedValue;
9use proc_macro_error::{abort, emit_call_site_error, emit_error, emit_warning, proc_macro_error};
10use quote::{quote, ToTokens};
11use syn::{
12    parse::{Parse, ParseStream},
13    spanned::Spanned,
14    Token,
15};
16
17use std::{collections::HashMap, fs::File, io::BufReader};
18
19use crate::{
20    generators::{GetResult, KeyGenerators, OverrideType},
21    schema::{KeySignature as SchemaKeySignature, SchemaList},
22};
23
24// TODO:
25// * Replace proc-macro-error dep with syn::Result
26// * Decouple enum and flags generation from key generation, make them standalone
27// * Use `quote_spanned` where applicable for better error propagation on generated code
28// * Remove serde and deluxe dependencies (consider using quick-xml directly or xmlserde)
29// * Improve enum generation (create enum based on its definition, instead of by key; also add doc alias for its id)
30// * Add way to map setter and getters value
31// * Add `bind_#key writable`, `user_#key_value`, `connect_#key_writable_changed` variants
32// * Add trybuild tests
33// * Support for multiple schema
34
35#[derive(deluxe::ParseMetaItem)]
36struct GenSettings {
37    file: SpannedValue<String>,
38    id: Option<SpannedValue<String>>,
39}
40
41#[derive(deluxe::ParseAttributes)]
42struct GenSettingsDefine {
43    signature: Option<SpannedValue<String>>,
44    key_name: Option<SpannedValue<String>>,
45    arg_type: SpannedValue<String>,
46    ret_type: SpannedValue<String>,
47}
48
49#[derive(deluxe::ParseAttributes)]
50struct GenSettingsSkip {
51    signature: Option<SpannedValue<String>>,
52    key_name: Option<SpannedValue<String>>,
53}
54
55struct SettingsStruct {
56    attrs: Vec<syn::Attribute>,
57    vis: syn::Visibility,
58    struct_token: Token![struct],
59    ident: syn::Ident,
60    semi_token: Token![;],
61}
62
63impl Parse for SettingsStruct {
64    fn parse(input: ParseStream<'_>) -> syn::parse::Result<Self> {
65        Ok(Self {
66            attrs: input.call(syn::Attribute::parse_outer)?,
67            vis: input.parse()?,
68            struct_token: input.parse()?,
69            ident: input.parse()?,
70            semi_token: input.parse()?,
71        })
72    }
73}
74
75impl ToTokens for SettingsStruct {
76    fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) {
77        self.vis.to_tokens(tokens);
78        self.struct_token.to_tokens(tokens);
79        self.ident.to_tokens(tokens);
80
81        let field: syn::FieldsUnnamed = syn::parse_quote!((gio::Settings));
82        field.to_tokens(tokens);
83
84        self.semi_token.to_tokens(tokens);
85    }
86}
87
88/// Macro for typesafe [`gio::Settings`] key access.
89///
90/// The macro's main purpose is to reduce the risk of mistyping a key,
91/// using the wrong method to access values, inputting incorrect values,
92/// and to reduce boilerplate. Additionally, the summary, description,
93/// and default value are included in the documentation of each generated
94/// method. This would be beneficial if you use tools like
95/// [`rust-analyzer`](https://rust-analyzer.github.io/).
96///
97/// **⚠️ IMPORTANT ⚠️**
98///
99/// Both `gio` and `glib` need to be in scope, so unless they are direct crate
100/// dependencies, you need to import them because `gen_settings` is using
101/// them internally. For example:
102///
103/// ```ignore
104/// use gtk::{gio, glib};
105/// ```
106///
107/// ### Example
108///
109/// ```ignore
110/// use gsettings_macro::gen_settings;
111///
112/// #[gen_settings(file = "./tests/io.github.seadve.test.gschema.xml")]
113/// pub struct ApplicationSettings;
114///
115/// let settings = ApplicationSettings::new("io.github.seadve.test");
116///
117/// // `i` D-Bus type
118/// settings.set_window_width(100);
119/// assert_eq!(settings.window_width(), 100);
120///
121/// // enums
122/// settings.set_alert_sound(AlertSound::Glass);
123/// assert_eq!(settings.alert_sound(), AlertSound::Glass);
124///
125/// // bitflags
126/// settings.set_space_style(SpaceStyle::BEFORE_COLON | SpaceStyle::BEFORE_COMMA);
127/// assert_eq!(
128///     settings.space_style(),
129///     SpaceStyle::BEFORE_COLON | SpaceStyle::BEFORE_COMMA
130/// );
131/// ```
132///
133/// Note: The file path is relative to the project root or where the
134/// `Cargo.toml` file is located.
135///
136/// ### Generated methods
137///
138/// The procedural macro generates the following [`gio::Settings`] methods
139/// for each key in the schema:
140///
141/// * `set` -> `set_${key}`, which panics when writing in a readonly
142/// key, and `try_set_${key}`, which behaves the same as the original method.
143/// * `get` -> `${key}`
144/// * `connect_changed` -> `connect_${key}_changed`
145/// * `bind` -> `bind_${key}`
146/// * `create_action` -> `create_${key}_action`
147/// * `default_value` -> `${key}_default_value`
148/// * `reset` -> `reset_${key}`
149///
150/// ### Known D-Bus type signatures
151///
152/// The setter and getter methods has the following parameter and
153/// return type, depending on the key's type signature.
154///
155/// | Type Signature | Parameter Type | Return Type   |
156/// | -------------- | -------------- | ------------- |
157/// | b              | `bool`         | `bool`        |
158/// | i              | `i32`          | `i32`         |
159/// | u              | `u32`          | `u32`         |
160/// | x              | `i64`          | `i64`         |
161/// | t              | `u64`          | `u64`         |
162/// | d              | `f64`          | `f64`         |
163/// | (ii)           | `(i32, i32`)   | `(i32, i32`)  |
164/// | as             | `&[&str]`      | `Vec<String>` |
165/// | s *            | `&str`         | `String`      |
166///
167/// \* If the key of type signature `s` has no `choice` attribute
168/// specified in the GSchema, the parameter and return types stated
169/// in the table would be applied. Otherwise, it will generate an
170/// enum, like described in the next section, and use it as the parameter
171/// and return types, instead of `&str` and `String` respectively.
172///
173/// It will not compile if the type signature is not defined above.
174/// However, it is possible to explicitly skip generating methods
175/// for a specific key or type signature using the attribute
176/// `#[gen_settings_skip]`, or define a custom parameter and return
177/// types using `#[gen_settings_define]` attribute. The usage of
178/// the latter will be further explained in the following sections.
179///
180/// ### Enums and Flags
181///
182/// The macro will also automatically generate enums or flags. If it is
183/// an enum, it would generated a normal Rust enum with each nick
184/// specified in the GSchema converted to pascal case as an enum variant.
185/// The enum would implement both [`ToVariant`] and [`FromVariant`], [`Clone`],
186/// [`Hash`], [`PartialEq`], [`Eq`], [`PartialOrd`], and [`Ord`]. On
187/// the other hand, if it is a flag, it would generate bitflags
188/// same as the bitflags generated by the [`bitflags`] macro with each
189/// nick specified in the GSchema converted to screaming snake case as
190/// a const flag.
191///
192/// The generated types, enum or bitflags, would have the same
193/// visibility and scope with the generated struct.
194///
195/// ### Skipping methods generation
196///
197/// This would be helpful if you want to have full control
198/// with the key without the macro intervening. For example:
199///
200/// ```ignore
201/// use gsettings_macro::gen_settings;
202///
203/// #[gen_settings(
204///     file = "./tests/io.github.seadve.test.gschema.xml",
205///     id = "io.github.seadve.test"
206/// )]
207/// // Skip generating methods for keys with type signature `(ss)`
208/// #[gen_settings_skip(signature = "(ss)")]
209/// // Skip generating methods for the key of name `some-key-name`
210/// #[gen_settings_skip(key_name = "some-key-name")]
211/// pub struct Settings;
212///
213/// impl Settings {
214///     pub fn set_some_key_name(value: &std::path::Path) {
215///         ...
216///     }
217/// }
218/// ```
219///
220/// ### Defining custom types
221///
222/// ```ignore
223/// use gsettings_macro::gen_settings;
224///
225/// use std::path::{Path, PathBuf};
226///
227/// #[gen_settings(file = "./tests/io.github.seadve.test.gschema.xml")]
228/// // Define custom parameter and return types for keys with type `(ss)`
229/// #[gen_settings_define(
230///     signature = "(ss)",
231///     arg_type = "(&str, &str)",
232///     ret_type = "(String, String)"
233/// )]
234/// // Define custom parameter and return types for key with name `cache-dir`
235/// #[gen_settings_define(key_name = "cache-dir", arg_type = "&Path", ret_type = "PathBuf")]
236/// pub struct SomeAppSettings;
237///
238/// let settings = SomeAppSettings::new("io.github.seadve.test");
239///
240/// settings.set_cache_dir(Path::new("/some_dir"));
241/// assert_eq!(settings.cache_dir(), PathBuf::from("/some_dir"));
242///
243/// settings.set_string_tuple(("hi", "hi2"));
244/// assert_eq!(settings.string_tuple(), ("hi".into(), "hi2".into()));
245/// ```
246///
247/// The type specified in `arg_type` and `ret_type` has to be on scope or
248/// you can specify the full path.
249///
250/// If you somehow do not want an enum parameter and return types for `s`
251/// type signature with choices. You can also use this to override that behavior.
252///
253/// Note: The type has to implement both [`ToVariant`] and [`FromVariant`] or it
254/// would fail to compile.
255///
256/// ### Default trait
257///
258/// The schema id can be specified as an attribute, making it implement
259/// [`Default`] and create a `new` constructor without parameters.
260/// Otherwise, it will not implement [`Default`] and would require the
261/// schema id as an parameter in the the constructor or the `new` method.
262///
263/// The following is an example of defining the `id` attribute in the macro:
264///
265/// ```ignore
266/// use gsettings_macro::gen_settings;
267///
268/// #[gen_settings(
269///     file = "./tests/io.github.seadve.test.gschema.xml",
270///     id = "io.github.seadve.test"
271/// )]
272/// pub struct ApplicationSettings;
273///
274/// // The id is specified above so it is not needed
275/// // to specify it in the constructor.
276/// let settings = ApplicationSettings::new();
277/// let another_instance = ApplicationSettings::default();
278/// ```
279///
280/// [`gio::Settings`]: https://docs.rs/gio/latest/gio/struct.Settings.html
281/// [`ToVariant`]: https://docs.rs/glib/latest/glib/variant/trait.ToVariant.html
282/// [`FromVariant`]: https://docs.rs/glib/latest/glib/variant/trait.FromVariant.html
283/// [`bitflags`]: https://docs.rs/bitflags/latest/bitflags/macro.bitflags.html
284#[proc_macro_attribute]
285#[proc_macro_error]
286pub fn gen_settings(
287    attr: proc_macro::TokenStream,
288    item: proc_macro::TokenStream,
289) -> proc_macro::TokenStream {
290    let GenSettings {
291        file: file_attr,
292        id: id_attr,
293    } = match deluxe::parse2(attr.into()) {
294        Ok(gen_settings) => gen_settings,
295        Err(err) => return err.to_compile_error().into(),
296    };
297    let file_attr_span = file_attr.span();
298    let schema_file_path = SpannedValue::into_inner(file_attr);
299
300    // Parse schema list
301    let schema_file = File::open(schema_file_path).unwrap_or_else(|err| {
302        abort!(file_attr_span, "failed to open schema file: {}", err);
303    });
304    let schema_list: SchemaList = quick_xml::de::from_reader(BufReader::new(schema_file))
305        .unwrap_or_else(|err| abort!(file_attr_span, "failed to parse schema file: {}", err));
306
307    // Get first schema
308    let mut schemas = schema_list.schemas;
309    if schemas.len() > 1 {
310        emit_warning!(file_attr_span, "this macro only supports a single schema");
311    }
312    let schema = schemas
313        .pop()
314        .unwrap_or_else(|| abort!(file_attr_span, "schema file must have a single schema"));
315
316    // Get schema id
317    let schema_id = if let Some(id_attr) = id_attr {
318        let id_attr_span = id_attr.span();
319        let schema_id = SpannedValue::into_inner(id_attr);
320
321        if schema.id != schema_id {
322            emit_error!(
323                id_attr_span,
324                "id does not match the one specified in the schema file"
325            );
326        }
327
328        Some(schema_id)
329    } else {
330        None
331    };
332
333    let settings_struct = syn::parse_macro_input!(item as SettingsStruct);
334
335    // Parse overrides
336    let known_signatures = schema
337        .keys
338        .iter()
339        .map(|key| {
340            key.signature().unwrap_or_else(|| {
341                abort!(file_attr_span, "expected one of `type`, `enum` or `flags` specified attribute on key `{}` in the schema", key.name);
342            })
343        })
344        .collect::<Vec<_>>();
345    let known_key_names = schema
346        .keys
347        .iter()
348        .map(|key| key.name.as_str())
349        .collect::<Vec<_>>();
350    let mut signature_overrides = HashMap::new();
351    let mut key_name_overrides = HashMap::new();
352    for attr in &settings_struct.attrs {
353        let (signature, key_name, override_type) = if attr.path().is_ident("gen_settings_define") {
354            let GenSettingsDefine {
355                signature,
356                key_name,
357                arg_type,
358                ret_type,
359            } = match deluxe::parse_attributes::<_, GenSettingsDefine>(attr) {
360                Ok(gen_settings) => gen_settings,
361                Err(err) => {
362                    emit_error!(attr.span(), err);
363                    continue;
364                }
365            };
366
367            (
368                signature,
369                key_name,
370                OverrideType::Define {
371                    arg_type: SpannedValue::into_inner(arg_type),
372                    ret_type: SpannedValue::into_inner(ret_type),
373                },
374            )
375        } else if attr.path().is_ident("gen_settings_skip") {
376            let GenSettingsSkip {
377                signature,
378                key_name,
379            } = match deluxe::parse_attributes::<_, GenSettingsSkip>(attr) {
380                Ok(gen_settings) => gen_settings,
381                Err(err) => {
382                    emit_error!(attr.span(), err);
383                    continue;
384                }
385            };
386
387            (signature, key_name, OverrideType::Skip)
388        } else {
389            emit_error!(
390                attr.span(),
391                "expected `#[gen_settings_define( .. )]` or `#[gen_settings_skip( .. )]`"
392            );
393            continue;
394        };
395
396        match (signature, key_name) {
397            (Some(_), Some(_)) => {
398                emit_error!(
399                    attr.span(),
400                    "cannot specify both `signature` and `key_name`"
401                )
402            }
403            (None, None) => {
404                emit_error!(attr.span(), "must specify either `signature` or `key_name`")
405            }
406            (Some(signature), None) => {
407                let signature_span = signature.span();
408                let signature_str = SpannedValue::into_inner(signature);
409                let signature_type = SchemaKeySignature::Type(signature_str);
410
411                if !known_signatures.contains(&signature_type) {
412                    emit_error!(signature_span, "useless define for this signature");
413                }
414
415                if signature_overrides.contains_key(&signature_type) {
416                    emit_error!(signature_span, "duplicate override");
417                }
418
419                signature_overrides.insert(signature_type, override_type);
420            }
421            (None, Some(key_name)) => {
422                let key_name_span = key_name.span();
423                let key_name_str = SpannedValue::into_inner(key_name);
424
425                if !known_key_names.contains(&key_name_str.as_str()) {
426                    emit_error!(key_name_span, "key_name not found in the schema");
427                }
428
429                if key_name_overrides.contains_key(&key_name_str) {
430                    emit_error!(key_name_span, "duplicate override");
431                }
432
433                key_name_overrides.insert(key_name_str, override_type);
434            }
435        }
436    }
437
438    // Generate keys
439    let enums = schema_list
440        .enums
441        .iter()
442        .map(|enum_| (enum_.id.to_string(), enum_))
443        .collect::<HashMap<_, _>>();
444    let flags = schema_list
445        .flags
446        .iter()
447        .map(|flag| (flag.id.to_string(), flag))
448        .collect::<HashMap<_, _>>();
449    let mut key_generators = KeyGenerators::with_defaults(enums, flags);
450    key_generators.add_signature_overrides(signature_overrides);
451    key_generators.add_key_name_overrides(key_name_overrides);
452
453    // Generate code
454    let mut aux_token_stream = proc_macro2::TokenStream::new();
455    let mut keys_token_stream = proc_macro2::TokenStream::new();
456
457    for key in &schema.keys {
458        match key_generators
459            .get(key, settings_struct.vis.clone())
460            .unwrap()
461        {
462            GetResult::Skip => (),
463            GetResult::Some(generator) => {
464                keys_token_stream.extend(generator.to_token_stream());
465
466                if let Some(aux) = generator.auxiliary() {
467                    aux_token_stream.extend(aux);
468                }
469            }
470            GetResult::Unknown => {
471                emit_call_site_error!(
472                    "unsupported {} signature used by key `{}`; consider using `#[gen_settings_define( .. )]` or skip it with `#[gen_settings_skip( .. )]`",
473                    &key.signature().unwrap(),
474                    &key.name,
475                )
476            }
477        }
478    }
479
480    let constructor_token_stream = if let Some(ref schema_id) = schema_id {
481        quote! {
482            pub fn new() -> Self {
483                Self(gio::Settings::new(#schema_id))
484            }
485        }
486    } else {
487        quote! {
488            pub fn new(schema_id: &str) -> Self {
489                Self(gio::Settings::new(schema_id))
490            }
491        }
492    };
493
494    let struct_ident = &settings_struct.ident;
495
496    let mut expanded = quote! {
497        #aux_token_stream
498
499        #[derive(Clone, Hash, PartialEq, Eq, gio::glib::ValueDelegate)]
500        #[value_delegate(nullable)]
501        #settings_struct
502
503        impl #struct_ident {
504            #constructor_token_stream
505
506            #keys_token_stream
507        }
508
509        impl std::ops::Deref for #struct_ident {
510            type Target = gio::Settings;
511
512            fn deref(&self) -> &Self::Target {
513                &self.0
514            }
515        }
516
517        impl std::ops::DerefMut for #struct_ident {
518            fn deref_mut(&mut self) -> &mut Self::Target {
519                &mut self.0
520            }
521        }
522
523        impl std::fmt::Debug for #struct_ident {
524            fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
525                std::fmt::Debug::fmt(&self.0, f)
526            }
527        }
528    };
529
530    if schema_id.is_some() {
531        expanded.extend(quote! {
532            impl Default for #struct_ident {
533                fn default() -> Self {
534                    Self::new()
535                }
536            }
537        });
538    }
539
540    expanded.into()
541}