Review My Derive Macro with Attributes

Code

# Project Structure

.
├── Cargo.toml
├── datameta-derive
│   ├── Cargo.toml
│   └── src
│       └── lib.rs
└── src
    ├── lib.rs
    └── main.rs
// src/main.rs

use datameta::DataMeta;

#[derive(DataMeta)]
#[datameta(author = "Eray Erdin", serial_version = 1)]
struct Foo {
    #[datameta(author = "Eray Erdin")]
    a: i32,
}

fn main() {
    let foo = Foo { a: 1 };

    println!("Foo author: {}", foo.author());
    println!("Foo version: {}", foo.serial_version());
    println!("Foo field authors: {:?}", foo.field_authors());
}
// src/lib.rs

use std::collections::HashMap;

pub use datameta_derive::DataMeta;

pub trait DataMeta {
    fn author(&self) -> &'static str;
    fn serial_version(&self) -> u8;
    fn field_authors(&self) -> HashMap<&'static str, &'static str>;
}
# Cargo.toml

[package]
name = "datameta"
version = "0.1.0"
edition = "2021"

[dependencies]
datameta-derive = { path = "datameta-derive" }
// datameta-derive/src/lib.rs

use std::collections::HashMap;

use syn::DeriveInput;

#[derive(deluxe::ExtractAttributes)]
#[deluxe(attributes(datameta))]
struct DataMetaStructAttributes {
    author: String,
    #[deluxe(default = 0)]
    serial_version: u8,
}

#[derive(deluxe::ExtractAttributes)]
struct DataMetaFieldAttributes {
    #[deluxe(default = "".into())]
    author: String,
}

fn extract_datameta_fields(
    ast: &mut DeriveInput,
) -> deluxe::Result<HashMap<String, DataMetaFieldAttributes>> {
    let mut field_attrs: HashMap<String, DataMetaFieldAttributes> = HashMap::new();
    if let syn::Data::Struct(s) = &mut ast.data {
        for field in s.fields.iter_mut() {
            let field_name = field.ident.as_ref().unwrap().to_string();
            let attrs: DataMetaFieldAttributes = deluxe::extract_attributes(field)?;
            field_attrs.insert(field_name, attrs);
        }
    }
    Ok(field_attrs)
}

fn datameta_modern_derive_macro(
    item: proc_macro2::TokenStream,
) -> deluxe::Result<proc_macro2::TokenStream> {
    // parse
    let mut ast: DeriveInput = syn::parse2(item)?;

    // extract struct attributes
    let DataMetaStructAttributes {
        author,
        serial_version,
    } = deluxe::extract_attributes(&mut ast)?;

    // extract field attributes
    let field_attrs: HashMap<String, DataMetaFieldAttributes> = extract_datameta_fields(&mut ast)?;
    let (fields, authors): (Vec<String>, Vec<String>) =
        field_attrs.into_iter().map(|(k, v)| (k, v.author)).unzip();

    // impl variables
    let ident = &ast.ident;
    let (impl_generics, type_generics, where_clause) = ast.generics.split_for_impl();

    // generate
    Ok(quote::quote! {
        impl DataMeta for #impl_generics #ident #type_generics #where_clause {
            fn author(&self) -> &'static str {
                #author
            }

            fn serial_version(&self) -> u8 {
                #serial_version
            }

            fn field_authors(&self) -> std::collections::HashMap<&'static str, &'static str> {
                let keys = vec![#(#fields),*];
                let authors = vec![#(#authors),*];
                let map: std::collections::HashMap<&'static str, &'static str> = keys.into_iter().zip(authors.into_iter()).collect();
                map
            }
        }
    })
}

#[proc_macro_derive(DataMeta, attributes(datameta))]
pub fn datameta_derive_macro(item: proc_macro::TokenStream) -> proc_macro::TokenStream {
    datameta_modern_derive_macro(item.into()).unwrap().into()
}
# datameta-derive/Cargo.toml

[package]
name = "datameta-derive"
version = "0.1.0"
edition = "2021"

[dependencies]
deluxe = "0.5.0"
proc-macro2 = "1.0.56"
quote = "1.0.26"
syn = "2.0.15"

[lib]
proc-macro = true

Explanation and Questions

I've been trying to understand derive macros with attributes. In the code above, what I wanted to do was to add custom attribute to the whole struct Foo and each fields.

This is just a throw-away project, just for fun and learning. I want to get your general opinion about it. Other than that, some questions I have are:

  • While using deluxe, do I have to rely on proc-macro2? Their docs always provide samples with proc_macro2::TokenSteam.
  • I've used let (fields, authors): (Vec<String>, Vec<String>) = field_attrs.into_iter().map(|(k, v)| (k, v.author)).unzip(); in order to get HashMap to two different Vec<String>s so that I can convert it to tokens in generated field_authors method. Do you know a better way?
  • Generated field_authors method returns std::collections::HashMap<&'static str, &'static str>, do you know a better approach to this?

Thanks in advance.

Yes, procedural macro parser/helper crates almost always use proc-macro2.

You could simplify this by omitting the temporary variable and the .into_iter() call. You don't need the vec!, either – it allocates unnecessarily, you could have just used an array as well, because the items are known at compile time:

fn field_authors(&self) -> std::collections::HashMap<&'static str, &'static str> {
    let keys = [#(#fields),*];
    let authors = [#(#authors),*];
    keys.into_iter().zip(authors).collect()
}

It depends heavily on what you want to use it for.


Another general comment is that you should probably not unwrap() in a library. Use into_compile_error() instead.

2 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.