pogo_masterfile_macros/
lib.rs1use proc_macro::TokenStream;
7use quote::quote;
8use syn::{Data, DeriveInput, Fields, parse_macro_input};
9
10#[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#[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#[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 pub fn template_id(&self) -> &str {
209 match self {
210 #(#arms),*
211 }
212 }
213 }
214 }
215 .into()
216}
217
218#[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
295fn 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 let _ = meta.input;
311 }
312 Ok(())
313 })?;
314 if let Some(s) = found {
315 return Ok(Some(s));
316 }
317 }
318 Ok(None)
319}