Best way to parse key value attribute in derive macro?

Hi, I'm building a derive macro and was wondering what is the best way to parse a struct/enum attribute that has key value args.

Input

#[derive(Example)]
#[example(a = 5, b = "test")]
struct Struct;

How I'm currently doing it

struct Args {
    a: LitInt,
    b: LitStr,
}

impl Parse for Args {
    fn parse(tokens: ParseStream) -> Result<Self> {
        let mut a = None;
        let mut b = None;

        let parsed = Punctuated::<MetaNameValue, Token![,]>::parse_terminated(tokens)?;
        for kv in parsed.clone() {
            if kv.path.is_ident("a") {
                if a.is_some() {
                    bail!(kv, "`a` provided twice");
                } else if let Lit::Int(v) = kv.lit {
                    a = Some(v);
                } else {
                    bail!(kv.lit, "`a` value should be an integer");
                }
            } else if kv.path.is_ident("b") {
                if b.is_some() {
                    bail!(kv, "`b` provided twice");
                } else if let Lit::Str(v) = kv.lit {
                    b = Some(v);
                } else {
                    bail!(kv.lit, "`b` value should be a string");
                }
            }
        }

        match (a, b) {
            (Some(a), Some(b)) => Ok(Self { a, b }),
            (None, None) => bail!(parsed, "missing both `a` and `b`"),
            (None, _) => bail!(parsed, "missing `a`"),
            (_, None) => bail!(parsed, "missing `b`"),
        }
    }
}

fn parse_args(input: &DeriveInput) -> Result<Args> {
    for attr in &input.attrs {
        if attr.path.is_ident("example") {
            return attr.parse_args();
        }
    }
    bail!(
        input,
        "#[derive(Example)] requires an `#[example(..)]` attribute"
    );
}

Is there a better way of doing this? I also tried to use syn::AttributeArgs but I couldn't figure out how to get that from attr.tokens.


There is also https://docs.rs/darling, which seems to have some more features, but at the cost of a way harder to understand API (imho, although it may just be an issue with the main's page documentation).


You can also write and bundle your own "mini-bae":

Implementation:
macro_rules! derive_Parse {(
    @derive_only
    $( #[$attr:meta] )*
    $pub:vis
    struct $StructName:ident {
        $(
            $( #[$field_attr:meta] )*
            $field_pub:vis
            $field_name:ident : $FieldTy:ty
        ),* $(,)?
    }
) => (
    impl ::syn::parse::Parse for $StructName {
        fn parse (input: ::syn::parse::ParseStream<'_>)
          -> ::syn::Result<Self>
        {
            mod kw {
                $(
                    ::syn::custom_keyword!( $field_name );
                )*
            }
            use ::core::ops::Not as _;

            $(
                let mut $field_name = ::core::option::Option::None::< $FieldTy >;
            )*
            while input.is_empty().not() {
                let lookahead = input.lookahead1();
                match () {
                  $(
                    _case if lookahead.peek(kw::$field_name) => {
                        let span = input.parse::<kw::$field_name>().unwrap().span;
                        let _: ::syn::Token![ = ] = input.parse()?;
                        let prev = $field_name.replace(input.parse()?);
                        if prev.is_some() {
                            return ::syn::Result::Err(::syn::Error::new(span, "Duplicate key"));
                        }
                    },
                  )*
                    _default => return ::syn::Result::Err(lookahead.error()),
                }
                let _: ::core::option::Option<::syn::Token![ , ]> = input.parse()?;
            }
            Ok(Self {
                $(
                    $field_name: $field_name.ok_or_else(|| ::syn::Error::new(
                        ::proc_macro2::Span::call_site(),
                        ::core::concat!("Missing key `", ::core::stringify!($field_name), "`"),
                    ))?,
                )*
            })
        }
    }
); (
    $( #[$attr:meta] )* $pub:vis struct $($rest:tt)*
) => (
    $( #[$attr] )* $pub struct $($rest)*

    derive_Parse! { @derive_only  $( #[$attr] )* $pub struct $($rest)* }
)} pub(crate) use derive_Parse;

▼ Usage:

derive_Parse! {
    struct Args {
        a: LitInt,
        b: LitStr,
    }
}
1 Like