Field attributes
Container attributes configure the whole type; field attributes configure a single field —
serde’s #[serde(rename = "...")], #[serde(skip)], and friends. The mechanics are the same
as the previous section, with one difference: you read the attributes off each Field instead
of off the DeriveInput.
One declaration, two positions
You still declare the helper attribute once, on the derive:
#![allow(unused)]
fn main() {
#[proc_macro_derive(Labelled, attributes(label))]
}
That single declaration covers the attribute wherever it legally appears — on the container and
on fields. Which positions you actually read is up to your macro. Here we read it per field, so
we look at field.attrs rather than input.attrs:
#![allow(unused)]
fn main() {
fn field_label(field: &syn::Field) -> syn::Result<String> {
for attr in &field.attrs {
if attr.path().is_ident("label") {
// ... read this field's label
}
}
// default: the field's own name
Ok(field.ident.as_ref().unwrap().to_string())
}
}
parse_args for a single value
The previous section used parse_nested_meta for a comma-separated key = value list. When the
attribute holds a single value — #[label("Email address")] — there’s a more direct tool:
Attribute::parse_args,
which parses the tokens inside the parentheses as any Parse type:
#![allow(unused)]
fn main() {
let s: syn::LitStr = attr.parse_args()?;
let label = s.value();
}
parse_args works with any type implementing syn::parse::Parse — LitStr, LitInt, Ident,
Type, or your own custom-parsed struct. It’s the right reach whenever an attribute carries one
self-contained argument.
Rust offers three attribute shapes, and syn has a tool for each:
- bare path —
#[label]— checkattr.path().is_ident(..) - name-value —
#[label = "x"]— read it withattr.meta.require_name_value() - list —
#[label(...)]— parsed with one of two tools, depending on its contents:- one self-contained argument —
#[label("x")]—attr.parse_args() - a
key = valuelist —#[label(a = 1, b = 2)]—attr.parse_nested_meta(..)
- one self-contained argument —
Picking the shape is a design choice; following one ecosystem’s convention — usually serde’s — keeps your macro familiar to users.
Reading a name-value attribute
parse_args is the easy path, but it only applies to the list shape. The name-value shape
#[label = "x"] doesn’t go through a parser — require_name_value() hands you a
MetaNameValue whose .value is a
plain syn::Expr, and you dig the literal out
yourself. A string literal arrives as Expr::Lit wrapping a Lit::Str, so you match those two
layers and reject anything else with a spanned error:
#![allow(unused)]
fn main() {
let nv = attr.meta.require_name_value()?;
match &nv.value {
syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(s), .. }) => s.value(),
other => return Err(syn::Error::new_spanned(other, "expected a string literal")),
}
}
Spanning the error on other (the offending expression) makes the diagnostic point at exactly
what the user wrote after the =, the same span discipline from the error-reporting sections.
Collecting fallible results
Computing one label per field is a map that can fail, so collect into a Result to
short-circuit on the first bad field:
#![allow(unused)]
fn main() {
let labels = fields
.iter()
.map(field_label)
.collect::<syn::Result<Vec<String>>>()?;
}
This is a handy idiom: collect() turns an iterator of Results into a Result of a Vec,
propagating the first Err.
Exercise
The exercise for this section is located in 04_derive/10_field_attributes
Labelled above used the list shape #[label("...")] with parse_args. For the exercise
you’ll build Renamed, which uses the name-value shape instead: #[rename = "..."]. It
generates a column_names() method returning each field’s name, overridden by #[rename] when
present. The iteration and require_name_value call are wired up; your job is to pull the string
out of the attribute’s value expression — a different job from parse_args, since here you
inspect a syn::Expr yourself (see the three-shapes aside above).