Search code examples
rustrust-macrosrust-proc-macros

Accept multiple values on proc macro attribute


I wanted to be able to retrieve the content from an attribute like this:

#[foreign_key(table = "some_table", column = "some_column")]

This is how I am trying:

impl TryFrom<&&Attribute> for EntityFieldAnnotation {
    type Error = syn::Error;

    fn try_from(attribute: &&Attribute) -> Result<Self, Self::Error> {
        if attribute.path.is_ident("foreign_key") {
            match attribute.parse_args()? {
                syn::Meta::NameValue(nv) => 
                    println!("NAME VALUE: {:?}, {:?}, {:?}", 
                        nv.path.get_ident(), 
                        nv.eq_token.to_token_stream(),
                        nv.lit.to_token_stream(),
                    ),
                _ => println!("Not interesting")
            }
        } else {
            println!("No foreign key")
        }

    // ... More Rust code
}

Everything works fine if I just put in there only one NameValue. When I add the comma, everything brokes.

The only error: error: unexpected token

How can I fix my logic to enable the possibility of have more than just one NameValue?

Thanks


Solution

  • UPDATE: While writing this answer, I had forgotten that Meta has List variant as well which gives you NestedMeta. I would generally prefer doing that instead of what I did in the answer below for more flexibility.

    Although, for your particular case, using Punctuated still seems simpler and cleaner to me.


    MetaNameValue represents only a single name-value pair. In your case it is delimited by ,, so, you need to parse all of those delimited values as MetaNameValue instead.

    Instead of calling parse_args, you can use parse_args_with along with Punctuated::parse_terminated:

    use syn::{punctuated::Punctuated, MetaNameValue, Token};
    
    let name_values: Punctuated<MetaNameValue, Token![,]> = attribute.parse_args_with(Punctuated::parse_terminated).unwrap(); // handle error instead of unwrap
    

    Above name_values has type Punctuated which is an iterator. You can iterate over it to get various MetaNameValue in your attribute.


    Updates based on comments:

    Getting value out as String from MetaNameValue:

    let name_values: Result<Punctuated<MetaNameValue, Token![,]>, _> = attr.parse_args_with(Punctuated::parse_terminated);
    
    match name_values {
        Ok(name_value) => {
            for nv in name_value {
                println!("Meta NV: {:?}", nv.path.get_ident());
                let value = match nv.lit {
                    syn::Lit::Str(v) => v.value(),
                    _ => panic!("expeced a string value"), // handle this err and don't panic
                };
                println!( "Meta VALUE: {:?}", value )
            }
        },
        Err(_) => todo!(),
    };