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}