on
Procedural Macros, pt. 6: Attributes
We’ve built our code, and
it’s passed all our tests.
But, we’ve just realized that we’d rather not have a getter generated for our
age
property. Thinking about it, the ability to skip generation of getters was
included in our original functional spec.
Enter, attributes.
Attributes provide a means to pass options or parameters to our derive macro
code. These options allow us to tweak the generation of code as needed. To skip
the generation of a getter, we are going to attach an optional skip
attribute
to a field.
#[derive(propertese)]
struct Person {
#[propertese(skip)]
name: String,
age: u32
}
The attribute can be passed in with a value if needed. For example, if our
derive macro were to be generating setters along with getters, we could pass in
#[propertese(skip="getter")]
. Alternatively, we could pass in a string literal
like #[propertese("skip_getter")]
. These variants could possibly instruct our
macro generation code to skip the generation of a getter while including a
setter. Depending on the form we choose, synstructure
will parse these out
into different types of meta items on syn::Field
.
#[propertese(skip)]
translates to asyn::Path
.#[propertese(skip="getter")]
would be parsed to asyn::MetaNameValue
.#[propertese("skip_getter")]
would be parsed to asyn::LitStr
.
Each of these are found in some nested property of an enum syn::NestedMeta
.
syn::Path
will suffice for our code generation. So, let’s add code to work
with that. To start with, let’s write up a function extract_attributes
that
will return any attributes attached to a field as a Vec<String>
.
fn extract_attributes(
field: &syn::Field,
span: proc_macro2::Span,
) -> Result<Vec<String>, ()> {
let mut attributes: Vec<String> = Vec::new();
let meta: Vec<syn::NestedMeta> = field
.attrs
.iter()
.filter\(|attr|
attr.path.is_ident("propertese"))
.map\(|attr| attr.parse\_meta\())
.filter_map(Result::ok)
.flat\_map\(|meta| match meta {
syn::Meta::List(meta_list) => meta_list.nested.into_iter().collect(),
_ => {
emit_error!(span, "expected #[propertese(...)]");
vec![]
}
})
.collect();
for meta_item in meta {
match &meta_item {
syn::NestedMeta::Meta(meta) => match meta {
syn::Meta::Path(path) => {
let attribute_name = path.get_ident().unwrap().to_string();
attributes.push(attribute_name);
}
_ => emit_error!(
span,
format!("#[propertese(...)] has invalid data: {:?}", meta)
),
},
_ => emit_error!(
span,
format!("literal strings as attributes are not supported")
),
}
}
Ok(attributes)
}
While the function looks like a bit much, it basically does the following
let meta: Vec<syn::NestedMeta = ...
This line of code filters out the attributes on a field that have an identity
propertese
. This is the keywordpropertese
from#[propertese(skip)]
. All attributes provided in asyn::Meta::List
are collected. We error out on other forms of attribute declarations.It then loops over each attribute and checks if it is of type
syn::Path
. If the check is passed, we add the name of the attribute to our result vector. All other forms of metadata are rejected with an error.
Armed with this code, we can modify our function derive_propertese
to use the
provided attributes.
match &s.ast().data {
syn::Data::Struct(data_struct) => match &data_struct.fields {
syn::Fields::Named\(fields\_named) => fields\_named.named.iter\().for\_each\(|field| {
let attributes = extract_attributes(field, span).unwrap();
if !attributes.iter\().any\(|e| e == \"skip\") {
let getter = derive_getter(field);
propertese_token_stream.extend(getter);
}
}),
_ => emit_error!(
span,
"macro propertese can only be called on structs with named fileds"
),
},
_ => emit_error!(span, "macro propertese can only be called on structs"),
}
With the above snippet, we first extract all the attributes (if any) that have
been attached to a field. We then check to see if the extracted list contains a
skip
. If it does, we skip generating the getter for that field.
Test again
Let’s add on to our unit tests to ensure the changes we’ve made work (as well as ensure we haven’t broken anything).
#[test]
fn test_getter_skip() {
synstructure::test_derive! {
derive_propertese {
struct Person {
#[propertese(skip)]
name: String,
age: u32
}
}
expands to {
impl Person {
fn get_age(&self) -> u32 {
self.age
}
}
}
no_build
}
}
```
The above test verifies the macro generates the correct code. Let’s add an usage
test as well. Create file tests/ui/test_skip.rs
with the following content
#[macro_use]
extern crate propertese;
#[derive(propertese)]
struct Person {
#[propertese(skip)]
name: String,
age: u32
}
fn main() {
let frank = Person {
name: "Frank".to_string(),
age: 40
};
assert_eq!(40, frank.get_age());
assert_eq!("Frank", frank.get_name());
}
Looking at the code, we see that we are trying to call frank.get_name()
. This
should generate a compilation error as we’ve instructed propertese
to skip the
generation of a getter for name
. To validate this, we will use the
compile_fail
feature of trybuild
. Edit tests/compile_tests.rs
to add this
check.
t.compile_fail("tests/ui/test_skip.rs");
The first time we run cargo test
we should see the test failing. trybuild
will create a file wip/test_skip.stderr
with the error that was returned by
the compiler. Examining it we see
error[E0599]: no method named `get_name` found for struct `Person` in the current scope
--> tests/ui/test_skip.rs\:18\:31
|
5 | struct Person {
| ------------- method `get_name` not found for this
...
18 | assert_eq!("Frank", frank.get_name());
| ^^^^^^^^ help: there is an associated function with a similar name: `get_age`
This is along expected lines. The error message hints to the availability of
get_age()
while stating that get_name()
does not exist. If the error were
not along expected lines, we’d edit our code until the compiler were to generate
the error we are looking for. Since, we have the right error, we can move
test_skip.stderr
to tests/ui/test_skip.stderr
. In subsequent tests runs,
trybuild
will compare the error message the compiler generates against the
contents of this file. If they match, it will pass the test case.
And, with that, we conclude our short series to writing procedural derive
macros in Rust.