#![doc = include_str!("../README.md")]
use std::collections::HashMap;
use std::sync::{LazyLock, Mutex};
use proc_macro::TokenStream;
use proc_macro2::TokenStream as TokenStream2;
use quote::quote;
use syn::{Data, DeriveInput, Fields, ItemStruct, parse_macro_input};
static FIELD_CACHE: LazyLock<Mutex<HashMap<String, String>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
#[proc_macro_derive(Fields)]
pub fn derive_fields(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let struct_name = input.ident.to_string();
let fields = match &input.data {
Data::Struct(data) => match &data.fields {
Fields::Named(named) => &named.named,
_ => panic!("Fields derive only supports structs with named fields"),
},
_ => panic!("Fields derive only supports structs"),
};
let field_tokens: Vec<TokenStream2> = fields
.iter()
.map(|f| {
let attrs = &f.attrs;
let vis = &f.vis;
let name = f.ident.as_ref().expect("named field must have ident");
let ty = &f.ty;
quote! {
#(#attrs)*
#vis #name: #ty,
}
})
.collect();
let content = quote! { struct __Fields { #(#field_tokens)* } }.to_string();
FIELD_CACHE
.lock()
.expect("FIELD_CACHE lock poisoned")
.insert(struct_name, content);
TokenStream::new()
}
#[proc_macro_attribute]
pub fn combine_fields(attr: TokenStream, item: TokenStream) -> TokenStream {
let source_names = parse_macro_input!(
attr with syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated
);
let input = parse_macro_input!(item as ItemStruct);
let struct_attrs = &input.attrs;
let struct_vis = &input.vis;
let struct_name = &input.ident;
let struct_generics = &input.generics;
let cache = FIELD_CACHE.lock().expect("FIELD_CACHE lock poisoned");
let mut all_fields: Vec<TokenStream2> = Vec::new();
for name in &source_names {
let key = name.to_string();
let content = cache.get(&key).unwrap_or_else(|| {
panic!(
"combine_fields: no cached fields for `{key}`.\n\
Make sure `{key}` derives `combine_structs::Fields` and its \
module is declared before the target struct."
)
});
let parsed: ItemStruct = syn::parse_str(content).unwrap_or_else(|e| {
panic!("combine_fields: failed to parse cached fields for `{key}`: {e}")
});
if let Fields::Named(named) = parsed.fields {
for field in named.named {
let attrs = &field.attrs;
let vis = &field.vis;
let ident = &field.ident;
let ty = &field.ty;
all_fields.push(quote! { #(#attrs)* #vis #ident: #ty, });
}
}
}
drop(cache);
if let Fields::Named(ref named) = input.fields {
for field in &named.named {
let attrs = &field.attrs;
let vis = &field.vis;
let ident = &field.ident;
let ty = &field.ty;
all_fields.push(quote! { #(#attrs)* #vis #ident: #ty, });
}
}
let expanded = quote! {
#(#struct_attrs)*
#struct_vis struct #struct_name #struct_generics {
#(#all_fields)*
}
};
expanded.into()
}