Skip to main content

pogo_masterfile_macros/
lib.rs

1//! Procedural derive macros backing the [`pogo-masterfile-types`] crate.
2//!
3//! This crate is normally consumed transparently via re-exports from
4//! `pogo-masterfile-types`. Direct dependency is fine but not required.
5
6use proc_macro::TokenStream;
7use quote::quote;
8use syn::{Data, DeriveInput, Fields, parse_macro_input};
9
10/// Derives `pub const ALL: [Self; N]` and `pub const SIZE: usize` for a
11/// unit-only enum. Visibility of the constants follows the enum's own
12/// visibility.
13///
14/// ```
15/// use pogo_masterfile_macros::AllVariants;
16///
17/// #[derive(AllVariants)]
18/// enum E { A, B, C }
19///
20/// assert_eq!(E::SIZE, 3);
21/// ```
22#[proc_macro_derive(AllVariants)]
23pub fn derive_all_variants(input: TokenStream) -> TokenStream {
24    let input = parse_macro_input!(input as DeriveInput);
25    let name = &input.ident;
26    let vis = &input.vis;
27
28    let Data::Enum(data_enum) = &input.data else {
29        return syn::Error::new_spanned(name, "AllVariants only applies to enums")
30            .to_compile_error()
31            .into();
32    };
33
34    let mut errors: Vec<syn::Error> = Vec::new();
35    let mut variant_idents: Vec<&syn::Ident> = Vec::new();
36    for v in &data_enum.variants {
37        match &v.fields {
38            Fields::Unit => variant_idents.push(&v.ident),
39            _ => errors.push(syn::Error::new_spanned(
40                v,
41                "AllVariants requires all variants to be unit (no fields)",
42            )),
43        }
44    }
45
46    if !errors.is_empty() {
47        let combined = errors
48            .into_iter()
49            .reduce(|mut a, b| {
50                a.combine(b);
51                a
52            })
53            .unwrap();
54        return combined.to_compile_error().into();
55    }
56
57    let count = variant_idents.len();
58    let qualified = variant_idents.iter().map(|v| quote! { #name::#v });
59
60    quote! {
61        impl #name {
62            #vis const SIZE: usize = #count;
63            #vis const ALL: [Self; #count] = [ #(#qualified),* ];
64        }
65    }
66    .into()
67}
68
69/// Derives `pub const fn as_str(&self) -> &'static str` and `impl Display`
70/// for a unit-only enum. Each variant's string is taken from a
71/// `#[serde(rename = "...")]` attribute; if absent, falls back to
72/// `stringify!(VariantIdent)`.
73///
74/// Declares `serde` as a helper attribute so `#[serde(rename = "...")]`
75/// is syntactically recognized even when serde itself is not derived on
76/// the same enum. Production use will always co-derive Serialize /
77/// Deserialize too, but the macro stays usable in isolation (e.g. tests).
78#[proc_macro_derive(AsStr, attributes(serde))]
79pub fn derive_as_str(input: TokenStream) -> TokenStream {
80    let input = parse_macro_input!(input as DeriveInput);
81    let name = &input.ident;
82
83    let Data::Enum(data_enum) = &input.data else {
84        return syn::Error::new_spanned(name, "AsStr only applies to enums")
85            .to_compile_error()
86            .into();
87    };
88
89    let mut errors: Vec<syn::Error> = Vec::new();
90    let mut arms: Vec<proc_macro2::TokenStream> = Vec::new();
91    for v in &data_enum.variants {
92        if !matches!(v.fields, Fields::Unit) {
93            errors.push(syn::Error::new_spanned(
94                v,
95                "AsStr requires all variants to be unit (no fields)",
96            ));
97            continue;
98        }
99        let ident = &v.ident;
100        let lit = match extract_serde_rename(&v.attrs) {
101            Ok(Some(s)) => s,
102            Ok(None) => ident.to_string(),
103            Err(e) => {
104                errors.push(e);
105                continue;
106            }
107        };
108        arms.push(quote! { Self::#ident => #lit });
109    }
110
111    if !errors.is_empty() {
112        let combined = errors
113            .into_iter()
114            .reduce(|mut a, b| {
115                a.combine(b);
116                a
117            })
118            .unwrap();
119        return combined.to_compile_error().into();
120    }
121
122    quote! {
123        impl #name {
124            pub const fn as_str(&self) -> &'static str {
125                match self {
126                    #(#arms),*
127                }
128            }
129        }
130        impl ::core::fmt::Display for #name {
131            fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result {
132                f.write_str(self.as_str())
133            }
134        }
135    }
136    .into()
137}
138
139/// Derives an inherent `template_id(&self) -> &str` method for an enum
140/// whose every variant is a single-field tuple wrapping a struct with a
141/// `template_id: String` field.
142///
143/// Eliminates boilerplate `match` arms when dispatching on a wide enum
144/// (e.g. `MasterfileEntry`) to read the inner `template_id` — every arm
145/// is mechanically `Self::Variant(e) => e.template_id.as_str()`.
146///
147/// # Requirements
148///
149/// - The type is an enum.
150/// - Every variant is a single-field tuple variant: `Variant(Inner)`.
151/// - The inner type has a `template_id` field of a type that exposes
152///   `.as_str() -> &str` (i.e. `String` or `&str`).
153///
154/// # Example
155///
156/// ```ignore
157/// use pogo_masterfile_macros::TemplateId;
158///
159/// struct Inner { template_id: String }
160///
161/// #[derive(TemplateId)]
162/// enum E { A(Inner), B(Inner) }
163///
164/// let e = E::A(Inner { template_id: "X".into() });
165/// assert_eq!(e.template_id(), "X");
166/// ```
167#[proc_macro_derive(TemplateId)]
168pub fn derive_template_id(input: TokenStream) -> TokenStream {
169    let input = parse_macro_input!(input as DeriveInput);
170    let name = &input.ident;
171
172    let Data::Enum(data_enum) = &input.data else {
173        return syn::Error::new_spanned(name, "TemplateId only applies to enums")
174            .to_compile_error()
175            .into();
176    };
177
178    let mut errors: Vec<syn::Error> = Vec::new();
179    let mut arms: Vec<proc_macro2::TokenStream> = Vec::new();
180
181    for v in &data_enum.variants {
182        let ident = &v.ident;
183        match &v.fields {
184            Fields::Unnamed(fields) if fields.unnamed.len() == 1 => {
185                arms.push(quote! { Self::#ident(inner) => inner.template_id.as_str() });
186            }
187            _ => errors.push(syn::Error::new_spanned(
188                v,
189                "TemplateId requires every variant to be a single-field tuple wrapping a struct with `template_id: String`",
190            )),
191        }
192    }
193
194    if !errors.is_empty() {
195        let combined = errors
196            .into_iter()
197            .reduce(|mut a, b| {
198                a.combine(b);
199                a
200            })
201            .unwrap();
202        return combined.to_compile_error().into();
203    }
204
205    quote! {
206        impl #name {
207            /// Read the `template_id` of whichever variant `self` is.
208            pub fn template_id(&self) -> &str {
209                match self {
210                    #(#arms),*
211                }
212            }
213        }
214    }
215    .into()
216}
217
218/// Derives `impl FromStr` AND `impl TryFrom<&str>` for a unit-only enum.
219/// Both share the same string-matching logic: `#[serde(rename = "...")]`
220/// first, variant ident otherwise. The error type is
221/// `pogo_masterfile_types::UnknownTemplateId` — the macro emits a path
222/// reference; consumers must have that type in scope (which they do
223/// transparently via the parent crate).
224///
225/// `TryFrom<&str>` is needed for callers using
226/// `impl TryInto<TemplateId>`-style polymorphic input (e.g.
227/// `pogo-masterfile`'s per-group accessor `get` method): the std blanket
228/// `impl<T, U: TryFrom<T>> TryInto<U> for T` lets `&str` and a typed enum
229/// both satisfy a single `I: TryInto<TemplateId>` bound at the call site.
230#[proc_macro_derive(FromStrEnum, attributes(serde))]
231pub fn derive_from_str_enum(input: TokenStream) -> TokenStream {
232    let input = parse_macro_input!(input as DeriveInput);
233    let name = &input.ident;
234
235    let Data::Enum(data_enum) = &input.data else {
236        return syn::Error::new_spanned(name, "FromStrEnum only applies to enums")
237            .to_compile_error()
238            .into();
239    };
240
241    let mut errors: Vec<syn::Error> = Vec::new();
242    let mut arms: Vec<proc_macro2::TokenStream> = Vec::new();
243    for v in &data_enum.variants {
244        if !matches!(v.fields, Fields::Unit) {
245            errors.push(syn::Error::new_spanned(
246                v,
247                "FromStrEnum requires all variants to be unit (no fields)",
248            ));
249            continue;
250        }
251        let ident = &v.ident;
252        let lit = match extract_serde_rename(&v.attrs) {
253            Ok(Some(s)) => s,
254            Ok(None) => ident.to_string(),
255            Err(e) => {
256                errors.push(e);
257                continue;
258            }
259        };
260        arms.push(quote! { #lit => Ok(Self::#ident) });
261    }
262
263    if !errors.is_empty() {
264        let combined = errors
265            .into_iter()
266            .reduce(|mut a, b| {
267                a.combine(b);
268                a
269            })
270            .unwrap();
271        return combined.to_compile_error().into();
272    }
273
274    quote! {
275        impl ::core::str::FromStr for #name {
276            type Err = pogo_masterfile_types::UnknownTemplateId;
277            fn from_str(s: &str) -> ::core::result::Result<Self, Self::Err> {
278                match s {
279                    #(#arms),*,
280                    other => Err(pogo_masterfile_types::UnknownTemplateId(other.to_string())),
281                }
282            }
283        }
284
285        impl ::core::convert::TryFrom<&str> for #name {
286            type Error = pogo_masterfile_types::UnknownTemplateId;
287            fn try_from(s: &str) -> ::core::result::Result<Self, Self::Error> {
288                <Self as ::core::str::FromStr>::from_str(s)
289            }
290        }
291    }
292    .into()
293}
294
295/// Look for `#[serde(rename = "...")]` on a variant. Returns the string
296/// payload if found. Errors only on malformed serde attributes.
297fn extract_serde_rename(attrs: &[syn::Attribute]) -> syn::Result<Option<String>> {
298    for attr in attrs {
299        if !attr.path().is_ident("serde") {
300            continue;
301        }
302        let mut found: Option<String> = None;
303        attr.parse_nested_meta(|meta| {
304            if meta.path.is_ident("rename") {
305                let value = meta.value()?;
306                let s: syn::LitStr = value.parse()?;
307                found = Some(s.value());
308            } else {
309                // Skip other serde meta items (e.g., `untagged`, `tag = "..."`).
310                let _ = meta.input;
311            }
312            Ok(())
313        })?;
314        if let Some(s) = found {
315            return Ok(Some(s));
316        }
317    }
318    Ok(None)
319}