Crate proc_micro

Source
Expand description

§Proc Micro

Small conveniences for high-quality macros.

§Use

$ cargo add proc_micro
$ cargo add strum --features=derive

§Errors

Normal rust code returns on the first error. Great macros accumulate as many errors as they can and show them all at once.

  • MaybeError: A container that holds zero or more syn::Error-s. When it holds error(s) they can be accumulated into a single syn::Error (which is a data structure for holding one or more macro errors).
  • OkMaybe: An alternative for Result that allows for returning both data and (maybe) errors at the same time. Use Result when an error means the data is unusable (untrustable) or OkMaybe when an error means the partial data might still be useful in generating additional error. information. The caller can convert an OkMaybe into a Result, but cannot convert a Result into an OkMaybe.

§Enum powered Attribute parsing

These helpers work with attributes defined as enums with this library:

§Tutorial

Here’s how you define a macro attribute that has a namespace of my_macro and accepts rename = <string> and ignore attributes using the strum crate:

const NAMESPACE: proc_micro::AttrNamespace =
    proc_micro::AttrNamespace("my_macro");

#[derive(strum::EnumDiscriminants, Debug, PartialEq)]
#[strum_discriminants(
    name(KnownAttribute),
    derive(strum::EnumIter, strum::Display, strum::EnumString, Hash)
)]
enum ParseAttribute {
    // #[my_macro(rename = "<string>")]
    #[allow(non_camel_case_types)]
    rename(String),
    // #[my_macro(ignore)]
    #[allow(non_camel_case_types)]
    ignore,
}

impl syn::parse::Parse for ParseAttribute {
    fn parse(input: syn::parse::ParseStream) -> syn::Result<Self> {
        let ident = input.parse::<syn::Ident>()?;
        match proc_micro::known_attribute(&ident)? {
            KnownAttribute::rename => {
                input.parse::<syn::Token![=]>()?;
                Ok(ParseAttribute::rename(
                    input.parse::<syn::LitStr>()?.value(),
                ))
            }
            KnownAttribute::ignore => Ok(ParseAttribute::ignore),
        }
    }
}

Each parsed attribute is stored in our enum while the discriminant can be used as a lookup. By representing attributes as an enum, we can be confident our code handles attribute additions or modifications exhaustively.

This also provides a platform for unit testing attribute logic:

let attribute = syn::parse_str::<ParseAttribute>(
    "rename = \"Ruby version\""
).unwrap();
assert_eq!(ParseAttribute::rename("Ruby version".to_string()), attribute);

Then within your macro code you can convert many comma separated attributes into enums while accumulating errors:


let mut errors = proc_micro::MaybeError::new();

let field: syn::Field = syn::parse_quote! {
    #[my_macro(ignore, rename = "Ruby version")]
    version: String
};

let attributes: Vec<WithSpan<ParseAttribute>> = proc_micro::parse_attrs(
    &NAMESPACE, &field.attrs
).push_unwrap(&mut errors);

assert_eq!(2, attributes.len());
assert!(matches!(attributes.first(), Some(WithSpan(ParseAttribute::ignore, _))));
assert!(errors.is_empty());

Use this result with other helpers to validate your attribute requirements. For example unique requires that attributes are specified at most once i.e. #[my_macro(ignore, ignore)] is incorrect. And check_exclusive is called for attributes that must be used exclusively, i.e. using “ignore” with any other attribute is in valid as they would have no effect. And you can use the returned WithSpan information to build your own custom syn::Error errors.


use proc_micro::OkMaybe;

// Make a structure to store your parsed configuration
#[derive(Debug, Clone)]
struct FieldConfig {
    ignore: bool,
    rename: Option<String>
}

// Use our building blocks to implement your desired logic
fn field_config(field: &syn::Field) -> OkMaybe<FieldConfig, syn::Error> {
    let mut rename_config = None;
    let mut ignore_config = false;
    let mut errors = proc_micro::MaybeError::new();

    let attributes: Vec<WithSpan<ParseAttribute>> = proc_micro::parse_attrs(
        &NAMESPACE, &field.attrs
    ).push_unwrap(&mut errors);

    proc_micro::check_exclusive(KnownAttribute::ignore, &attributes)
        .push_unwrap(&mut errors);
    let mut unique = proc_micro::unique(attributes)
        .push_unwrap(&mut errors);

    for (_, WithSpan(attribute, _)) in unique.drain() {
        match attribute {
            ParseAttribute::ignore => ignore_config = true,
            ParseAttribute::rename(name) => rename_config = Some(name),
        }
    }

    OkMaybe(
        FieldConfig {
            ignore: ignore_config,
            rename: rename_config
        },
        errors.maybe()
    )
}

// No problems
let _config = field_config(&syn::parse_quote! {
    #[my_macro(rename = "Ruby version")]
    version: String
}).to_result().unwrap();

// Problem with `check_exclusive`
let result = field_config(&syn::parse_quote! {
    #[my_macro(rename = "Ruby version", ignore)]
    version: String
}).to_result();

 assert!(result.is_err(), "Expected to be err but is {result:?}");
 let err = result.err().unwrap();
 assert_eq!(vec![
    "Exclusive attribute. Remove either `ignore` or `rename`".to_string(),
    "cannot be used with `ignore`".to_string()],
    err.into_iter().map(|e| e.to_string()).collect::<Vec<String>>()
 );

// Problem with `unique`
let result = field_config(&syn::parse_quote! {
    #[my_macro(ignore, ignore)]
    version: String
}).to_result();

assert!(result.is_err(), "Expected to be err but is {result:?}");
let err = result.err().unwrap();
assert_eq!(vec![
    "Duplicate attribute: `ignore`".to_string(),
    "previously `ignore` defined here".to_string()],
    err.into_iter().map(|e| e.to_string()).collect::<Vec<String>>()
);

// Multiple problems `unique` and unknown attribute
let result = field_config(&syn::parse_quote! {
    #[my_macro(ignore, ignore)]
    #[my_macro(unknown)]
    version: String
}).to_result();

assert!(result.is_err(), "Expected to be err but is {result:?}");
let err = result.err().unwrap();
assert_eq!(vec![
    "Unknown attribute: `unknown`. Must be one of `rename`, `ignore`".to_string(),
    "Duplicate attribute: `ignore`".to_string(),
    "previously `ignore` defined here".to_string(),
   ],
    err.into_iter().map(|e| e.to_string()).collect::<Vec<String>>()
);

Structs§

AttrNamespace
Represents the namespace of macro attributes
MaybeError
Accumulate zero or more syn::Error-s
OkMaybe
Represents generic data and maybe an error
WithSpan
Helper type for parsing a type and preserving the original span

Functions§

check_exclusive
Check exclusive attributes
known_attribute
Parses one bare word like “rename” for any iterable enum, and that’s it
parse_attrs
Parse attributes into a vector
unique
Guarantees all attributes (<k> = <v> or <v>) are specified only once