1#![doc = include_str!("../README.md")]
2
3use std::collections::HashMap;
4use std::sync::{LazyLock, Mutex};
5
6use proc_macro::TokenStream;
7use proc_macro2::TokenStream as TokenStream2;
8use quote::quote;
9use syn::{Data, DeriveInput, Fields, ItemStruct, parse_macro_input};
10
11static FIELD_CACHE: LazyLock<Mutex<HashMap<String, String>>> =
17 LazyLock::new(|| Mutex::new(HashMap::new()));
18
19#[proc_macro_derive(Fields)]
30pub fn derive_fields(input: TokenStream) -> TokenStream {
31 let input = parse_macro_input!(input as DeriveInput);
32 let struct_name = input.ident.to_string();
33
34 let fields = match &input.data {
35 Data::Struct(data) => match &data.fields {
36 Fields::Named(named) => &named.named,
37 _ => panic!("Fields derive only supports structs with named fields"),
38 },
39 _ => panic!("Fields derive only supports structs"),
40 };
41
42 let field_tokens: Vec<TokenStream2> = fields
44 .iter()
45 .map(|f| {
46 let attrs = &f.attrs;
47 let vis = &f.vis;
48 let name = f.ident.as_ref().expect("named field must have ident");
49 let ty = &f.ty;
50 quote! {
51 #(#attrs)*
52 #vis #name: #ty,
53 }
54 })
55 .collect();
56
57 let content = quote! { struct __Fields { #(#field_tokens)* } }.to_string();
59
60 FIELD_CACHE
61 .lock()
62 .expect("FIELD_CACHE lock poisoned")
63 .insert(struct_name, content);
64
65 TokenStream::new()
67}
68
69#[proc_macro_attribute]
91pub fn combine_fields(attr: TokenStream, item: TokenStream) -> TokenStream {
92 let source_names = parse_macro_input!(
93 attr with syn::punctuated::Punctuated::<syn::Ident, syn::Token![,]>::parse_terminated
94 );
95 let input = parse_macro_input!(item as ItemStruct);
96
97 let struct_attrs = &input.attrs;
98 let struct_vis = &input.vis;
99 let struct_name = &input.ident;
100 let struct_generics = &input.generics;
101
102 let cache = FIELD_CACHE.lock().expect("FIELD_CACHE lock poisoned");
103
104 let mut all_fields: Vec<TokenStream2> = Vec::new();
106
107 for name in &source_names {
108 let key = name.to_string();
109 let content = cache.get(&key).unwrap_or_else(|| {
110 panic!(
111 "combine_fields: no cached fields for `{key}`.\n\
112 Make sure `{key}` derives `combine_structs::Fields` and its \
113 module is declared before the target struct."
114 )
115 });
116
117 let parsed: ItemStruct = syn::parse_str(content).unwrap_or_else(|e| {
118 panic!("combine_fields: failed to parse cached fields for `{key}`: {e}")
119 });
120
121 if let Fields::Named(named) = parsed.fields {
122 for field in named.named {
123 let attrs = &field.attrs;
124 let vis = &field.vis;
125 let ident = &field.ident;
126 let ty = &field.ty;
127 all_fields.push(quote! { #(#attrs)* #vis #ident: #ty, });
128 }
129 }
130 }
131
132 drop(cache);
133
134 if let Fields::Named(ref named) = input.fields {
136 for field in &named.named {
137 let attrs = &field.attrs;
138 let vis = &field.vis;
139 let ident = &field.ident;
140 let ty = &field.ty;
141 all_fields.push(quote! { #(#attrs)* #vis #ident: #ty, });
142 }
143 }
144
145 let expanded = quote! {
146 #(#struct_attrs)*
147 #struct_vis struct #struct_name #struct_generics {
148 #(#all_fields)*
149 }
150 };
151
152 expanded.into()
153}