#macro-derive #attributes #proc-macro-attributes #deserialize #macro #derive

derive-attribute

A set of macros to automatically deserialize standard attributes

3 releases

0.1.2 Jun 1, 2023
0.1.1 May 29, 2023
0.1.0 May 29, 2023

#1551 in Rust patterns

30 downloads per month

MIT license

44KB
579 lines

Derive-Attribute   Latest Version Documentation

A set of macros to automatically deserialize standard attributes

  • Compatible with all major versions of Syn

  • Supports custom deserialization

  • Can return multiple errors at once

  • Allows for flexible attribute syntax

Syn Compatibility

This crate is meant to be used in conjunction with Syn within a procedural macro crate.
A major version of Syn can be selected as a feature like: features = ["syn_2"].

Note: A Syn version must be selected

Flexible Attribute Syntax

Implicit Booleans

#[some_attr(is_bool)] can also be written as #[some_attr(is_bool = true)]

Seperated Lists

#[some_attr(list(key_a = "value", key_b = 123))]
can also be written as
#[some_attr(list(key_a = "value"))]
#[some_attr(list(key_b = 123))]

Multiple Errors

Most macros will only return one attribute error at a time.
This crate's macros can return multiple errors at once resulting in a better developer experience.

Custom Deserialization

Any type that implements TryFromMeta can be used as a valid attribute type.
Although Its recommended that you use CustomArgFromMeta instead in order to simplify the implementation.

See example

Attr Arguments

The #[attr()] attribute can be added to the attribute struct or its fields to add additional options.

The full list of arguments are:

name [str] - Renames the field.

default [bool/str] - Uses a default value if the argument isn't found.
If its a boolean, the type's implementation of Default::default will be used.
If its a string, it must be a path to a function that returns the type.

Usage

Our attribute type is declared in a procedural macro crate:

#[derive(Attribute)]
#[attr(name = "my_attr")] // We set the attribute name to 'my_attr'
struct MyAttribute {      // Note: The attribute name will be the struct name in snake_case by default
    name: String,
    // wrapping a type in an option will make it optional
    list: Option<NestedList>, // deserializes a meta list named list i.e. list(num = 1)

    // booleans are always optional
    is_selected: bool,
}
    #[derive(List)]
    pub struct NestedList {
        num: Option<u8>
    }

It can then be used to parse the following attribute using the from_attrs method:

#[my_attr(name = "some_name", is_selected)]

lets look at the same attribute used in a derive macro

Basic derive

procedural macro crate:


use derive_attribute::{Attribute, List};
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};


#[derive(Attribute)]
#[attr(name = "my_attr")]
struct MyAttribute {
    name: String,
    // wrapping a type in an option will make it optional
    // deserializes a meta list named list i.e. list(num = 1) 
    list: Option<NestedList>,
    // booleans are always optional
    is_selected: bool,
}
    #[derive(List)]
    pub struct NestedList {
        num: Option<u8>
    }


#[proc_macro_derive(YOUR_MACRO_NAME, attributes(my_attr))]
pub fn derive_my_trait(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let ast = parse_macro_input!(tokens as DeriveInput);

    fn attempt_derive(ast: DeriveInput) -> Result<TokenStream2, Vec<syn::Error>> {
        // Wrapping an attribute in an option makes it optional
        // A missing error won't be returnwd
        let maybe_attribute = <Option<MyAttribute>>::from_attrs(ast.ident.span(), &ast.attrs)?;

        let output: TokenStream2 = {
            // Your Macro Generation Code
        };

        Ok(output)
    }


    let generated_tokens = 
        match attempt_derive(ast) {
            Ok(tokens) => tokens,
            Err(errors) => {
                let compile_errors = errors.into_iter().map(|e| e.to_compile_error());
                quote!(#(#compile_errors)*)
            }
        };

    generated_tokens.into()
}

Another crate using our macro

#[derive(YOUR_MACRO_NAME)]
#[my_attr(name = "some_name", is_selected)]
struct SomeStruct;

Now lets add our own argument type

Custom Deserialization

proc-macro crate:

use derive_attribute::{CustomArg, CustomArgFromMeta};

struct ErrorType {
    Warning,
    Severe
}

// Any type that implements 'TryFromMeta' can be deserialized however its a bit verbose
// In order to simplify the implementation we can implement 'CustomArgFromMeta' instead and wrap our type in the 'CustomArg' struct
impl<V: SynVersion> CustomArgFromMeta<V> for ErrorType {
    fn try_from_meta(meta: Self::Metadata) -> Result<Self, ErrorMsg> {
        let maybe_error_kind = 
            match V::deserialize_string(meta) {
                Some(string) => {
                    match string.to_string().as_str() {
                        "warning" => Some(Self::Warning),
                        "severe" => Some(Self::Severe),
                        _ => None
                    }
                }
                None => None
            };

        match maybe_error_kind {
            Some(error_kind) => Ok(error_kind),
            None => Err(InvalidType { expected: r#" "warning" or "severe" "# })
        }
    }
}

Our attribute struct now looks like this:

#[derive(Attribute)]
#[attr(name = "my_attr")]
struct MyAttribute {
    // In order to use the simplified trait(CustomArgFromMeta) we need to wrap our struct in 'CustomArg'
    error_type: CustomArg<ErrorType>,

    name: String,
    list: Option<u32>,
    is_selected: bool,
}

Another crate using our macro:

#[derive(YOUR_MACRO_NAME)]
#[error(error_type = "warning", name = "some_name", is_selected)]
struct Test;

Dependencies

~0.9–1.4MB
~27K SLoC