1use syn::{
2 Attribute, Expr, Lit, LitBool, LitStr, Meta, MetaList, Result, Token, Type, TypePath,
3 punctuated::Punctuated, spanned::Spanned,
4};
5
6fn find_attribute_metas(attrs: &[Attribute], attr_names: &[&str]) -> Result<Vec<MetaList>> {
8 let mut lists = Vec::new();
9 for attr in attrs {
10 let Some(attr_name) = attr_names
11 .iter()
12 .find(|attr_name| attr.path().is_ident(attr_name))
13 else {
14 continue;
15 };
16 match attr.meta.require_list() {
17 Ok(n) => lists.push(n.clone()),
18 _ => {
19 return Err(syn::Error::new(
20 attr.meta.span(),
21 format!("{attr_name} meta must specify a meta list"),
22 ));
23 }
24 }
25 }
26 Ok(lists)
27}
28
29fn get_meta_value<'a>(meta: &'a Meta, attr: &str) -> Result<&'a Lit> {
30 let meta = meta.require_name_value()?;
31 get_expr_lit(&meta.value, attr)
32}
33
34fn get_expr_lit<'a>(expr: &'a Expr, attr: &str) -> Result<&'a Lit> {
35 match expr {
36 Expr::Lit(l) => Ok(&l.lit),
37 Expr::Group(group) => get_expr_lit(&group.expr, attr),
39 expr => Err(syn::Error::new(
40 expr.span(),
41 format!("attribute `{attr}`'s value must be a literal"),
42 )),
43 }
44}
45
46pub fn match_attribute_with_str_value<'a>(
54 meta: &'a Meta,
55 attr: &str,
56) -> Result<Option<&'a LitStr>> {
57 if !meta.path().is_ident(attr) {
58 return Ok(None);
59 }
60
61 match get_meta_value(meta, attr)? {
62 Lit::Str(value) => Ok(Some(value)),
63 _ => Err(syn::Error::new(
64 meta.span(),
65 format!("value of the `{attr}` attribute must be a string literal"),
66 )),
67 }
68}
69
70pub fn match_attribute_with_bool_value<'a>(
78 meta: &'a Meta,
79 attr: &str,
80) -> Result<Option<&'a LitBool>> {
81 if meta.path().is_ident(attr) {
82 match get_meta_value(meta, attr)? {
83 Lit::Bool(value) => Ok(Some(value)),
84 other => Err(syn::Error::new(
85 other.span(),
86 format!("value of the `{attr}` attribute must be a boolean literal"),
87 )),
88 }
89 } else {
90 Ok(None)
91 }
92}
93
94pub fn match_attribute_with_str_list_value(meta: &Meta, attr: &str) -> Result<Option<Vec<String>>> {
95 if meta.path().is_ident(attr) {
96 let list = meta.require_list()?;
97 let values = list
98 .parse_args_with(Punctuated::<LitStr, Token![,]>::parse_terminated)?
99 .into_iter()
100 .map(|s| s.value())
101 .collect();
102
103 Ok(Some(values))
104 } else {
105 Ok(None)
106 }
107}
108
109pub fn match_attribute_without_value(meta: &Meta, attr: &str) -> Result<bool> {
116 if meta.path().is_ident(attr) {
117 meta.require_path_only()?;
118 Ok(true)
119 } else {
120 Ok(false)
121 }
122}
123
124pub fn iter_meta_lists(
127 attrs: &[Attribute],
128 list_names: &[&str],
129) -> Result<impl Iterator<Item = Meta>> {
130 let metas = find_attribute_metas(attrs, list_names)?
131 .into_iter()
132 .map(|meta| meta.parse_args_with(Punctuated::<Meta, Token![,]>::parse_terminated))
133 .collect::<Result<Vec<_>>>()?;
134
135 Ok(metas.into_iter().flatten())
136}
137
138#[macro_export]
283macro_rules! def_attrs {
284 (@attr_name crate_path $kind:tt) => { "crate" };
287 (@attr_name $attr_name:ident $kind:tt) => { ::std::stringify!($attr_name) };
288
289 (@attr_ty str) => {::std::option::Option<::std::string::String>};
290 (@attr_ty bool) => {::std::option::Option<bool>};
291 (@attr_ty [str]) => {::std::option::Option<::std::vec::Vec<::std::string::String>>};
292 (@attr_ty none) => {bool};
293 (@attr_ty {
294 $(#[$m:meta])*
295 $vis:vis $name:ident($what:literal) {
296 $($attr_name:ident $kind:tt),+
297 }
298 }) => {::std::option::Option<$name>};
299
300 (@match_attr_with $attr_name:ident, $meta:ident, $self:ident, $matched:expr, $display_name:expr) => {
301 if let ::std::option::Option::Some(value) = $matched? {
302 if $self.$attr_name.is_some() {
303 return ::std::result::Result::Err(::syn::Error::new(
304 $meta.span(),
305 ::std::format!("duplicate `{}` attribute", $display_name)
306 ));
307 }
308
309 $self.$attr_name = ::std::option::Option::Some(value.value());
310 return Ok(());
311 }
312 };
313
314 (@match_attr str crate_path, $meta:ident, $self:ident) => {
316 $crate::def_attrs!(
317 @match_attr_with
318 crate_path,
319 $meta,
320 $self,
321 $crate::macros::match_attribute_with_str_value($meta, "crate"),
322 "crate"
323 )
324 };
325 (@match_attr str $attr_name:ident, $meta:ident, $self:ident) => {
326 $crate::def_attrs!(
327 @match_attr_with
328 $attr_name,
329 $meta,
330 $self,
331 $crate::macros::match_attribute_with_str_value(
332 $meta,
333 ::std::stringify!($attr_name),
334 ),
335 ::std::stringify!($attr_name)
336 )
337 };
338 (@match_attr bool $attr_name:ident, $meta:ident, $self:ident) => {
339 $crate::def_attrs!(
340 @match_attr_with
341 $attr_name,
342 $meta,
343 $self,
344 $crate::macros::match_attribute_with_bool_value(
345 $meta,
346 ::std::stringify!($attr_name),
347 ),
348 ::std::stringify!($attr_name)
349 )
350 };
351 (@match_attr [str] $attr_name:ident, $meta:ident, $self:ident) => {
352 if let Some(list) = $crate::macros::match_attribute_with_str_list_value(
353 $meta,
354 ::std::stringify!($attr_name),
355 )? {
356 if $self.$attr_name.is_some() {
357 return ::std::result::Result::Err(::syn::Error::new(
358 $meta.span(),
359 concat!("duplicate `", stringify!($attr_name), "` attribute")
360 ));
361 }
362
363 $self.$attr_name = Some(list);
364 return Ok(());
365 }
366 };
367 (@match_attr none $attr_name:ident, $meta:ident, $self:ident) => {
368 if $crate::macros::match_attribute_without_value(
369 $meta,
370 ::std::stringify!($attr_name),
371 )? {
372 if $self.$attr_name {
373 return ::std::result::Result::Err(::syn::Error::new(
374 $meta.span(),
375 concat!("duplicate `", stringify!($attr_name), "` attribute")
376 ));
377 }
378
379 $self.$attr_name = true;
380 return Ok(());
381 }
382 };
383 (@match_attr {
384 $(#[$m:meta])*
385 $vis:vis $name:ident($what:literal) $body:tt
386 } $attr_name:ident, $meta:expr, $self:ident) => {
387 if $meta.path().is_ident(::std::stringify!($attr_name)) {
388 if $self.$attr_name.is_some() {
389 return ::std::result::Result::Err(::syn::Error::new(
390 $meta.span(),
391 concat!("duplicate `", stringify!($attr_name), "` attribute")
392 ));
393 }
394
395 return match $meta {
396 ::syn::Meta::List(meta) => {
397 $self.$attr_name = ::std::option::Option::Some($name::parse_nested_metas(
398 meta.parse_args_with(::syn::punctuated::Punctuated::<::syn::Meta, ::syn::Token![,]>::parse_terminated)?
399 )?);
400 ::std::result::Result::Ok(())
401 }
402 ::syn::Meta::Path(_) => {
403 $self.$attr_name = ::std::option::Option::Some($name::default());
404 ::std::result::Result::Ok(())
405 }
406 ::syn::Meta::NameValue(_) => Err(::syn::Error::new(
407 $meta.span(),
408 ::std::format!(::std::concat!(
409 "attribute `", ::std::stringify!($attr_name),
410 "` must be either a list or a path"
411 )),
412 ))
413 };
414 }
415 };
416 (@def_ty str) => {};
417 (@def_ty bool) => {};
418 (@def_ty [str]) => {};
419 (@def_ty none) => {};
420 (
421 @def_ty {
422 $(#[$m:meta])*
423 $vis:vis $name:ident($what:literal) {
424 $($attr_name:ident $kind:tt),+
425 }
426 }
427 ) => {
428 $($crate::def_attrs!(@def_ty $kind);)+
430
431 $crate::def_attrs!(
432 @def_struct
433 $(#[$m])*
434 $vis $name($what) {
435 $($attr_name $kind),+
436 }
437 );
438 };
439 (
440 @def_struct
441 $(#[$m:meta])*
442 $vis:vis $name:ident($what:literal) {
443 $($attr_name:ident $kind:tt),+
444 }
445 ) => {
446 $(#[$m])*
447 #[derive(Default, Clone, Debug)]
448 $vis struct $name {
449 $(pub $attr_name: $crate::def_attrs!(@attr_ty $kind)),+
450 }
451
452 impl $name {
453 pub fn parse_meta(
454 &mut self,
455 meta: &::syn::Meta
456 ) -> ::syn::Result<()> {
457 use ::syn::spanned::Spanned;
458
459 $(
462 $crate::def_attrs!(@match_attr $kind $attr_name, meta, self);
463 )+
464
465 let err = if ALLOWED_ATTRS.iter().any(|attr| meta.path().is_ident(attr)) {
467 ::std::format!(
468 ::std::concat!("attribute `{}` is not allowed on ", $what),
469 meta.path().get_ident().unwrap()
470 )
471 } else {
472 ::std::format!("unknown attribute `{}`", meta.path().get_ident().unwrap())
473 };
474 return ::std::result::Result::Err(::syn::Error::new(meta.span(), err));
475 }
476
477 pub fn parse_nested_metas<I>(iter: I) -> syn::Result<Self>
478 where
479 I: ::std::iter::IntoIterator<Item=::syn::Meta>
480 {
481 let mut parsed = $name::default();
482 for nested_meta in iter {
483 parsed.parse_meta(&nested_meta)?;
484 }
485
486 Ok(parsed)
487 }
488
489 pub fn parse_with_lists(
490 attrs: &[::syn::Attribute],
491 lists: &[&str],
492 ) -> ::syn::Result<Self> {
493 let mut parsed = $name::default();
494
495 for nested_meta in $crate::macros::iter_meta_lists(attrs, lists)? {
496 parsed.parse_meta(&nested_meta)?;
497 }
498
499 Ok(parsed)
500 }
501
502 pub fn parse(attrs: &[::syn::Attribute]) -> ::syn::Result<Self> {
503 Self::parse_with_lists(attrs, ALLOWED_LISTS)
504 }
505 }
506 };
507 (
508 crate $($list_name:ident),+;
509 $(
510 $(#[$m:meta])*
511 $vis:vis $name:ident($what:literal) {
512 $($attr_name:ident $kind:tt),+
513 }
514 );+;
515 ) => {
516 static ALLOWED_ATTRS: &[&'static str] = &[
517 $($($crate::def_attrs!(@attr_name $attr_name $kind),)+)+
518 ];
519
520 static ALLOWED_LISTS: &[&'static str] = &[
521 $(::std::stringify!($list_name),)+
522 ];
523
524 $(
525 $crate::def_attrs!(
526 @def_ty {
527 $(#[$m])*
528 $vis $name($what) {
529 $($attr_name $kind),+
530 }
531 }
532 );
533 )+
534 }
535}
536
537pub fn ty_is_option(ty: &Type) -> bool {
539 match ty {
540 Type::Path(TypePath {
541 path: syn::Path { segments, .. },
542 ..
543 }) => segments.last().unwrap().ident == "Option",
544 _ => false,
545 }
546}
547
548#[cfg(test)]
549mod tests {
550 use syn::{DeriveInput, Meta, parse_quote};
551
552 crate::def_attrs! {
553 crate zvariant, zgvariant;
554
555 pub TestAttributes("test item") { signature str, rename_all str };
556 }
557
558 #[test]
559 fn parse_with_lists_merges_all_matching_lists() {
560 let input: DeriveInput = parse_quote! {
561 #[zvariant(signature = "a{sv}")]
562 #[zgvariant(rename_all = "camelCase")]
563 struct Foo;
564 };
565 let attrs =
566 TestAttributes::parse_with_lists(&input.attrs, &["zvariant", "zgvariant"]).unwrap();
567 assert_eq!(attrs.signature.as_deref(), Some("a{sv}"));
568 assert_eq!(attrs.rename_all.as_deref(), Some("camelCase"));
569 }
570
571 #[test]
572 fn parse_with_lists_errors_on_cross_namespace_duplicate() {
573 let input: DeriveInput = parse_quote! {
574 #[zvariant(signature = "s")]
575 #[zgvariant(signature = "s")]
576 struct Foo;
577 };
578 let err =
579 TestAttributes::parse_with_lists(&input.attrs, &["zvariant", "zgvariant"]).unwrap_err();
580 assert!(err.to_string().contains("duplicate"));
581 }
582
583 #[test]
584 fn parse_with_lists_ignores_unlisted_namespaces() {
585 let input: DeriveInput = parse_quote! {
586 #[zgvariant(signature = "s")]
587 struct Foo;
588 };
589 let attrs = TestAttributes::parse_with_lists(&input.attrs, &["zvariant"]).unwrap();
590 assert!(attrs.signature.is_none());
591 }
592
593 #[test]
594 fn parse_uses_default_lists() {
595 let input: DeriveInput = parse_quote! {
596 #[zvariant(signature = "s")]
597 struct Foo;
598 };
599 let attrs = TestAttributes::parse(&input.attrs).unwrap();
600 assert_eq!(attrs.signature.as_deref(), Some("s"));
601 }
602
603 #[test]
604 fn parse_nested_metas_builds_from_metas() {
605 let meta: Meta = parse_quote!(signature = "s");
606 let attrs = TestAttributes::parse_nested_metas(vec![meta]).unwrap();
607 assert_eq!(attrs.signature.as_deref(), Some("s"));
608 }
609}