Parsing attributes with darling
The last two sections parsed attributes by hand: loop over attrs, match on path(), call
parse_nested_meta, pull out values, raise errors for anything unexpected. It works, but it’s a
lot of fiddly, repetitive code — and it grows fast as you add options. darling
is a crate that generates all of that for you from a plain struct, the same way serde generates
(de)serialization.
Describe the attributes as a struct
With darling you declare the shape of your attributes as a Rust struct and derive the
parsing. There are derives mirroring each level of the input:
FromDeriveInput— the whole item, including container attributes.FromField— a single field.FromVariant— an enum variant.FromMeta— a single value, so you can parse attributes into your own enums and types.
#![allow(unused)]
fn main() {
use darling::{FromDeriveInput, FromField, ast, util::Ignored};
#[derive(FromDeriveInput)]
#[darling(attributes(model), supports(struct_named))]
struct ModelOpts {
ident: syn::Ident,
data: ast::Data<Ignored, FieldOpts>,
#[darling(default = "default_table")]
table: String,
}
fn default_table() -> String {
"items".to_string()
}
#[derive(FromField)]
#[darling(attributes(model))]
struct FieldOpts {
ident: Option<syn::Ident>,
rename: Option<String>, // `Option<T>` is optional automatically
}
}
A few things darling is doing here:
- Magic fields.
ident,data,attrs,generics,visare recognised by name and filled from the input —identis the type name,datais the body. ast::Data<V, F>captures the body generically:Vis the per-variant type (hereIgnored, since we only support structs),Fis the per-field type. Later you call.take_struct()to get the fields back asFieldOptsvalues.supports(struct_named)rejects enums, tuple structs, and unit structs for you, with a proper error — no hand-writtenmatchonData.- Optional options. An
Option<T>field (likerename) is optional automatically — it defaults toNonewith no annotation. For a non-Optionfield you opt in with#[darling(default)](falls back to the type’sDefault) or#[darling(default = "...")](calls a named function).tableuses the latter:#[darling(default = "default_table")]callsdefault_table(), which returns"items", when the user omits#[model(table = "...")]. Unknown keys and type mismatches become precisedarling::Errors automatically.
Wire it into the macro
Parsing is then a single fallible call, and darling::Error renders itself straight to
compile_error! tokens:
#![allow(unused)]
fn main() {
#[proc_macro_derive(Model, attributes(model))]
pub fn model(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
model_impl(&input)
.unwrap_or_else(|err| err.write_errors())
.into()
}
fn model_impl(input: &DeriveInput) -> darling::Result<proc_macro2::TokenStream> {
let opts = ModelOpts::from_derive_input(input)?;
// ... use opts.ident, opts.table, opts.data.take_struct(), ...
}
}
Compare this to section 9: the same configurability, but the parsing, defaulting, validation, and
error reporting are all generated. For anything beyond one or two trivial options, darling is
what real macro crates reach for.
Exercise
The exercise for this section is located in 04_derive/11_darling
Complete the Model derive macro. The darling schema (ModelOpts and FieldOpts) is written
for you — study it. Your job is the code generation: call from_derive_input, then emit
table_name() and columns() from the parsed options, using each field’s rename when present.