Skip to main content

combine_structs/
lib.rs

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
11/// In-memory cache of field definitions, keyed by struct name.
12///
13/// Both [`derive_fields`] and [`combine_fields`] run in the same proc-macro
14/// dylib process during a single crate compilation.  `#[derive(Fields)]`
15/// writes here; `#[combine_fields]` reads.
16static FIELD_CACHE: LazyLock<Mutex<HashMap<String, String>>> =
17    LazyLock::new(|| Mutex::new(HashMap::new()));
18
19/// Derive macro that caches a struct's field definitions for later merging
20/// by [`combine_fields`].
21///
22/// The field definitions (including all attributes, doc comments, and
23/// visibility) are stored in an in-memory cache shared with
24/// `#[combine_fields]`.
25///
26/// # Panics
27///
28/// Panics if applied to an enum or union (only named structs are supported).
29#[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    // Collect each field's tokens: attributes + vis + name + type
43    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    // Serialize as a parseable struct so combine_fields can read it back.
58    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    // No output tokens — all communication happens via the in-memory cache.
66    TokenStream::new()
67}
68
69/// Attribute macro that merges fields from other `#[derive(Fields)]` structs
70/// into the annotated struct.
71///
72/// # Example
73///
74/// ```ignore
75/// #[combine_fields(CoreVocabulary, ApplicatorVocabulary)]
76/// #[derive(Debug, Clone, Default)]
77/// pub struct Schema {
78///     // extra fields defined here
79///     pub markdown_description: Option<String>,
80/// }
81/// ```
82///
83/// The macro reads cached field definitions (stored by `#[derive(Fields)]`)
84/// and emits the target struct with all fields merged in.
85///
86/// # Panics
87///
88/// Panics if the attribute arguments are not a comma-separated list of
89/// identifiers, or if the annotated item is not a struct with named fields.
90#[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    // Collect fields from each source struct's cached definition.
105    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    // Append the target struct's own fields.
135    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}