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
44KB
579 lines
Derive-Attribute
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