zvariant_utils/
macros.rs

1use syn::{
2    spanned::Spanned, Attribute, Lit, LitBool, LitStr, Meta, MetaList, NestedMeta, Result, Type,
3    TypePath,
4};
5
6// find the #[@attr_name] attribute in @attrs
7fn find_attribute_meta(attrs: &[Attribute], attr_name: &str) -> Result<Option<MetaList>> {
8    let meta = match attrs.iter().find(|a| a.path.is_ident(attr_name)) {
9        Some(a) => a.parse_meta(),
10        _ => return Ok(None),
11    }?;
12    match meta {
13        Meta::List(n) => Ok(Some(n)),
14        _ => Err(syn::Error::new(
15            meta.span(),
16            format!("{attr_name} meta must specify a meta list"),
17        )),
18    }
19}
20
21fn get_meta_value<'a>(meta: &'a Meta, attr: &str) -> Result<&'a Lit> {
22    match meta {
23        Meta::NameValue(meta) => Ok(&meta.lit),
24        Meta::Path(_) => Err(syn::Error::new(
25            meta.span(),
26            format!("attribute `{attr}` must have a value"),
27        )),
28        Meta::List(_) => Err(syn::Error::new(
29            meta.span(),
30            format!("attribute {attr} is not a list"),
31        )),
32    }
33}
34
35/// Compares `ident` and `attr` and in case they match ensures `value` is `Some` and contains a
36/// [`struct@LitStr`]. Returns `true` in case `ident` and `attr` match, otherwise false.
37///
38/// # Errors
39///
40/// Returns an error in case `ident` and `attr` match but the value is not `Some` or is not a
41/// [`struct@LitStr`].
42pub fn match_attribute_with_str_value<'a>(
43    meta: &'a Meta,
44    attr: &str,
45) -> Result<Option<&'a LitStr>> {
46    if meta.path().is_ident(attr) {
47        match get_meta_value(meta, attr)? {
48            Lit::Str(value) => Ok(Some(value)),
49            _ => Err(syn::Error::new(
50                meta.span(),
51                format!("value of the `{attr}` attribute must be a string literal"),
52            )),
53        }
54    } else {
55        Ok(None)
56    }
57}
58
59/// Compares `ident` and `attr` and in case they match ensures `value` is `Some` and contains a
60/// [`struct@LitBool`]. Returns `true` in case `ident` and `attr` match, otherwise false.
61///
62/// # Errors
63///
64/// Returns an error in case `ident` and `attr` match but the value is not `Some` or is not a
65/// [`struct@LitBool`].
66pub fn match_attribute_with_bool_value<'a>(
67    meta: &'a Meta,
68    attr: &str,
69) -> Result<Option<&'a LitBool>> {
70    if meta.path().is_ident(attr) {
71        match get_meta_value(meta, attr)? {
72            Lit::Bool(value) => Ok(Some(value)),
73            other => Err(syn::Error::new(
74                other.span(),
75                format!("value of the `{attr}` attribute must be a boolean literal"),
76            )),
77        }
78    } else {
79        Ok(None)
80    }
81}
82
83pub fn match_attribute_with_str_list_value(meta: &Meta, attr: &str) -> Result<Option<Vec<String>>> {
84    if meta.path().is_ident(attr) {
85        match meta {
86            Meta::List(list) => {
87                let mut values = Vec::with_capacity(list.nested.len());
88
89                for meta in &list.nested {
90                    values.push(match meta {
91                        NestedMeta::Lit(Lit::Str(lit)) => Ok(lit.value()),
92                        NestedMeta::Lit(lit) => Err(syn::Error::new(
93                            lit.span(),
94                            format!("invalid literal type for `{attr}` attribute"),
95                        )),
96                        NestedMeta::Meta(meta) => Err(syn::Error::new(
97                            meta.span(),
98                            format!("`{attr}` attribute must be a list of string literals"),
99                        )),
100                    }?)
101                }
102
103                Ok(Some(values))
104            }
105            _ => Err(syn::Error::new(
106                meta.span(),
107                format!("invalid meta type for attribute `{attr}`"),
108            )),
109        }
110    } else {
111        Ok(None)
112    }
113}
114
115/// Compares `ident` and `attr` and in case they match ensures `value` is `None`. Returns `true` in
116/// case `ident` and `attr` match, otherwise false.
117///
118/// # Errors
119///
120/// Returns an error in case `ident` and `attr` match but the value is not `None`.
121pub fn match_attribute_without_value(meta: &Meta, attr: &str) -> Result<bool> {
122    if meta.path().is_ident(attr) {
123        match meta {
124            Meta::Path(_) => Ok(true),
125            Meta::List(_) => Err(syn::Error::new(
126                meta.span(),
127                format!("attribute {attr} is not a list"),
128            )),
129            Meta::NameValue(_) => Err(syn::Error::new(
130                meta.span(),
131                format!("attribute `{attr}` must not have a value"),
132            )),
133        }
134    } else {
135        Ok(false)
136    }
137}
138
139/// Returns an iterator over the contents of all [`MetaList`]s with the specified identifier in an
140/// array of [`Attribute`]s.
141pub fn iter_meta_lists(
142    attrs: &[Attribute],
143    list_name: &str,
144) -> Result<impl Iterator<Item = NestedMeta>> {
145    let meta = find_attribute_meta(attrs, list_name)?;
146
147    Ok(meta.into_iter().flat_map(|meta| meta.nested.into_iter()))
148}
149
150/// Generates one or more structures used for parsing attributes in proc macros.
151///
152/// Generated structures have one static method called parse that accepts a slice of [`Attribute`]s.
153/// The method finds attributes that contain meta lists (look like `#[your_custom_ident(...)]`) and
154/// fills a newly allocated structure with values of the attributes if any.
155///
156/// The expected input looks as follows:
157///
158/// ```
159/// # use zvariant_utils::def_attrs;
160/// def_attrs! {
161///     crate zvariant;
162///
163///     /// A comment.
164///     pub StructAttributes("struct") { foo str, bar str, baz none };
165///     #[derive(Hash)]
166///     FieldAttributes("field") { field_attr bool };
167/// }
168/// ```
169///
170/// Here we see multiple entries: an entry for an attributes group called `StructAttributes` and
171/// another one for `FieldAttributes`. The former has three defined attributes: `foo`, `bar` and
172/// `baz`. The generated structures will look like this in that case:
173///
174/// ```
175/// /// A comment.
176/// #[derive(Default, Clone, Debug)]
177/// pub struct StructAttributes {
178///     foo: Option<String>,
179///     bar: Option<String>,
180///     baz: bool,
181/// }
182///
183/// #[derive(Hash)]
184/// #[derive(Default, Clone, Debug)]
185/// struct FieldAttributes {
186///     field_attr: Option<bool>,
187/// }
188/// ```
189///
190/// `foo` and `bar` attributes got translated to fields with `Option<String>` type which contain the
191/// value of the attribute when one is specified. They are marked with `str` keyword which stands
192/// for string literals. The `baz` attribute, on the other hand, has `bool` type because it's an
193/// attribute without value marked by the `none` keyword.
194///
195/// Currently the following literals are supported:
196///
197/// * `str` - string literals;
198/// * `bool` - boolean literals;
199/// * `[str]` - lists of string literals (`#[macro_name(foo("bar", "baz"))]`);
200/// * `none` - no literal at all, the attribute is specified alone.
201///
202/// The strings between braces are embedded into error messages produced when an attribute defined
203/// for one attribute group is used on another group where it is not defined. For example, if the
204/// `field_attr` attribute was encountered by the generated `StructAttributes::parse` method, the
205/// error message would say that it "is not allowed on structs".
206///
207/// # Nested attribute lists
208///
209/// It is possible to create nested lists for specific attributes. This is done as follows:
210///
211/// ```
212/// # use zvariant_utils::def_attrs;
213/// def_attrs! {
214///     crate zvariant;
215///
216///     pub OuterAttributes("outer") {
217///         simple_attr bool,
218///         nested_attr {
219///             /// An example of nested attributes.
220///             pub InnerAttributes("inner") {
221///                 inner_attr str
222///             }
223///         }
224///     };
225/// }
226/// ```
227///
228/// The syntax for inner attributes is the same as for the outer attributes, but you can specify
229/// only one inner attribute per outer attribute.
230///
231/// # Calling the macro multiple times
232///
233/// The macro generates an array called `ALLOWED_ATTRS` that contains a list of allowed attributes.
234/// Calling the macro twice in the same scope will cause a name alias and thus will fail to compile.
235/// You need to place each macro invocation into a module in that case.
236///
237/// # Errors
238///
239/// The generated parse method checks for some error conditions:
240///
241/// 1. Unknown attributes. When multiple attribute groups are defined in the same macro invocation,
242/// one gets a different error message when providing an attribute from a different attribute group.
243/// 2. Duplicate attributes.
244/// 3. Missing attribute value or present attribute value when none is expected.
245/// 4. Invalid literal type for attributes with values.
246#[macro_export]
247macro_rules! def_attrs {
248    (@attr_ty str) => {::std::option::Option<::std::string::String>};
249    (@attr_ty bool) => {::std::option::Option<bool>};
250    (@attr_ty [str]) => {::std::option::Option<::std::vec::Vec<::std::string::String>>};
251    (@attr_ty none) => {bool};
252    (@attr_ty {
253        $(#[$m:meta])*
254        $vis:vis $name:ident($what:literal) {
255            $($attr_name:ident $kind:tt),+
256        }
257    }) => {::std::option::Option<$name>};
258    (@match_attr_with $attr_name:ident, $meta:ident, $self:ident, $matched:expr) => {
259        if let ::std::option::Option::Some(value) = $matched? {
260            if $self.$attr_name.is_none() {
261                $self.$attr_name = ::std::option::Option::Some(value.value());
262                return Ok(());
263            } else {
264                return ::std::result::Result::Err(::syn::Error::new(
265                    $meta.span(),
266                    ::std::concat!("duplicate `", ::std::stringify!($attr_name), "` attribute")
267                ));
268            }
269        }
270    };
271    (@match_attr str $attr_name:ident, $meta:ident, $self:ident) => {
272        $crate::def_attrs!(
273            @match_attr_with
274            $attr_name,
275            $meta,
276            $self,
277            $crate::macros::match_attribute_with_str_value(
278                $meta,
279                ::std::stringify!($attr_name),
280            )
281        )
282    };
283    (@match_attr bool $attr_name:ident, $meta:ident, $self:ident) => {
284        $crate::def_attrs!(
285            @match_attr_with
286            $attr_name,
287            $meta,
288            $self,
289            $crate::macros::match_attribute_with_bool_value(
290                $meta,
291                ::std::stringify!($attr_name),
292            )
293        )
294    };
295    (@match_attr [str] $attr_name:ident, $meta:ident, $self:ident) => {
296        if let Some(list) = $crate::macros::match_attribute_with_str_list_value(
297            $meta,
298            ::std::stringify!($attr_name),
299        )? {
300            if $self.$attr_name.is_none() {
301                $self.$attr_name = Some(list);
302                return Ok(());
303            } else {
304                return ::std::result::Result::Err(::syn::Error::new(
305                    $meta.span(),
306                    concat!("duplicate `", stringify!($attr_name), "` attribute")
307                ));
308            }
309        }
310    };
311    (@match_attr none $attr_name:ident, $meta:ident, $self:ident) => {
312        if $crate::macros::match_attribute_without_value(
313            $meta,
314            ::std::stringify!($attr_name),
315        )? {
316            if !$self.$attr_name {
317                $self.$attr_name = true;
318                return Ok(());
319            } else {
320                return ::std::result::Result::Err(::syn::Error::new(
321                    $meta.span(),
322                    concat!("duplicate `", stringify!($attr_name), "` attribute")
323                ));
324            }
325        }
326    };
327    (@match_attr {
328        $(#[$m:meta])*
329        $vis:vis $name:ident($what:literal) $body:tt
330    } $attr_name:ident, $meta:expr, $self:ident) => {
331        if $meta.path().is_ident(::std::stringify!($attr_name)) {
332            return if $self.$attr_name.is_none() {
333                match $meta {
334                    ::syn::Meta::List(meta) => {
335                        $self.$attr_name = ::std::option::Option::Some($name::parse_nested_metas(
336                            meta.nested.iter()
337                        )?);
338                        ::std::result::Result::Ok(())
339                    }
340                    ::syn::Meta::Path(_) => {
341                        $self.$attr_name = ::std::option::Option::Some($name::default());
342                        ::std::result::Result::Ok(())
343                    }
344                    ::syn::Meta::NameValue(_) => Err(::syn::Error::new(
345                        $meta.span(),
346                        ::std::format!(::std::concat!(
347                            "attribute `", ::std::stringify!($attr_name),
348                            "` must be either a list or a path"
349                        )),
350                    ))
351                }
352            } else {
353                ::std::result::Result::Err(::syn::Error::new(
354                    $meta.span(),
355                    concat!("duplicate `", stringify!($attr_name), "` attribute")
356                ))
357            }
358        }
359    };
360    (@def_ty $list_name:ident str) => {};
361    (@def_ty $list_name:ident bool) => {};
362    (@def_ty $list_name:ident [str]) => {};
363    (@def_ty $list_name:ident none) => {};
364    (
365        @def_ty $list_name:ident {
366            $(#[$m:meta])*
367            $vis:vis $name:ident($what:literal) {
368                $($attr_name:ident $kind:tt),+
369            }
370        }
371    ) => {
372        // Recurse further to potentially define nested lists.
373        $($crate::def_attrs!(@def_ty $attr_name $kind);)+
374
375        $crate::def_attrs!(
376            @def_struct
377            $list_name
378            $(#[$m])*
379            $vis $name($what) {
380                $($attr_name $kind),+
381            }
382        );
383    };
384    (
385        @def_struct
386        $list_name:ident
387        $(#[$m:meta])*
388        $vis:vis $name:ident($what:literal) {
389            $($attr_name:ident $kind:tt),+
390        }
391    ) => {
392        $(#[$m])*
393        #[derive(Default, Clone, Debug)]
394        $vis struct $name {
395            $(pub $attr_name: $crate::def_attrs!(@attr_ty $kind)),+
396        }
397
398        impl $name {
399            pub fn parse_meta(
400                &mut self,
401                meta: &::syn::Meta
402            ) -> ::syn::Result<()> {
403                use ::syn::spanned::Spanned;
404
405                // This creates subsequent if blocks for simplicity. Any block that is taken
406                // either returns an error or sets the attribute field and returns success.
407                $(
408                    $crate::def_attrs!(@match_attr $kind $attr_name, meta, self);
409                )+
410
411                // None of the if blocks have been taken, return the appropriate error.
412                let is_valid_attr = ALLOWED_ATTRS.iter().any(|attr| meta.path().is_ident(attr));
413                return ::std::result::Result::Err(::syn::Error::new(meta.span(), if is_valid_attr {
414                    ::std::format!(
415                        ::std::concat!("attribute `{}` is not allowed on ", $what),
416                        meta.path().get_ident().unwrap()
417                    )
418                } else {
419                    ::std::format!("unknown attribute `{}`", meta.path().get_ident().unwrap())
420                }))
421            }
422
423            pub fn parse_nested_metas<'a, I>(iter: I) -> syn::Result<Self>
424            where
425                I: ::std::iter::IntoIterator<Item=&'a ::syn::NestedMeta>
426            {
427                let mut parsed = $name::default();
428                for nested_meta in iter {
429                    match nested_meta {
430                        ::syn::NestedMeta::Meta(meta) => parsed.parse_meta(meta),
431                        ::syn::NestedMeta::Lit(lit) => {
432                            ::std::result::Result::Err(::syn::Error::new(
433                                lit.span(),
434                                ::std::concat!(
435                                    "attribute `", ::std::stringify!($list_name),
436                                    "` does not support literals in meta lists"
437                                )
438                            ))
439                        }
440                    }?;
441                }
442
443                Ok(parsed)
444            }
445
446            pub fn parse(attrs: &[::syn::Attribute]) -> ::syn::Result<Self> {
447                let mut parsed = $name::default();
448                for nested_meta in $crate::macros::iter_meta_lists(attrs, ::std::stringify!($list_name))? {
449                    match &nested_meta {
450                        ::syn::NestedMeta::Meta(meta) => parsed.parse_meta(meta),
451                        ::syn::NestedMeta::Lit(lit) => {
452                            ::std::result::Result::Err(::syn::Error::new(
453                                lit.span(),
454                                ::std::concat!(
455                                    "attribute `", ::std::stringify!($list_name),
456                                    "` does not support literals in meta lists"
457                                )
458                            ))
459                        }
460                    }?;
461                }
462
463                Ok(parsed)
464            }
465        }
466    };
467    (
468        crate $list_name:ident;
469        $(
470            $(#[$m:meta])*
471            $vis:vis $name:ident($what:literal) {
472                $($attr_name:ident $kind:tt),+
473            }
474        );+;
475    ) => {
476        static ALLOWED_ATTRS: &[&'static str] = &[
477            $($(::std::stringify!($attr_name),)+)+
478        ];
479
480        $(
481            $crate::def_attrs!(
482                @def_ty
483                $list_name {
484                    $(#[$m])*
485                    $vis $name($what) {
486                        $($attr_name $kind),+
487                    }
488                }
489            );
490        )+
491    }
492}
493
494/// Checks if a [`Type`]'s identifier is "Option".
495pub fn ty_is_option(ty: &Type) -> bool {
496    match ty {
497        Type::Path(TypePath {
498            path: syn::Path { segments, .. },
499            ..
500        }) => segments.last().unwrap().ident == "Option",
501        _ => false,
502    }
503}