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 onproc-macro2
? Their docs always provide samples withproc_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 getHashMap
to two differentVec<String>
s so that I can convert it to tokens in generatedfield_authors
method. Do you know a better way? - Generated
field_authors
method returnsstd::collections::HashMap<&'static str, &'static str>
, do you know a better approach to this?
Thanks in advance.