lib.rs (7503B)
1 /* 2 This file is part of TALER 3 Copyright (C) 2026 Taler Systems SA 4 5 TALER is free software; you can redistribute it and/or modify it under the 6 terms of the GNU Affero General Public License as published by the Free Software 7 Foundation; either version 3, or (at your option) any later version. 8 9 TALER is distributed in the hope that it will be useful, but WITHOUT ANY 10 WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR 11 A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. 12 13 You should have received a copy of the GNU Affero General Public License along with 14 TALER; see the file COPYING. If not, see <http://www.gnu.org/licenses/> 15 */ 16 17 use proc_macro::TokenStream; 18 use quote::quote; 19 use syn::{Data, DeriveInput, Error, Expr, Lit, Meta, parse_macro_input}; 20 21 #[proc_macro_derive(EnumMeta, attributes(enum_meta, code))] 22 pub fn derive_domain_code(input: TokenStream) -> TokenStream { 23 let input = parse_macro_input!(input as DeriveInput); 24 let name = &input.ident; 25 26 // Parse features 27 let mut enabled_doc = false; 28 let mut enabled_domain_code = false; 29 let mut enabled_str = false; 30 31 for attr in &input.attrs { 32 if attr.path().is_ident("enum_meta") 33 && let Err(e) = attr.parse_nested_meta(|meta| { 34 if meta.path.is_ident("Description") { 35 enabled_doc = true; 36 } else if meta.path.is_ident("DomainCode") { 37 enabled_domain_code = true; 38 } else if meta.path.is_ident("Str") { 39 enabled_str = true; 40 } else { 41 return Err(meta.error("unknown enum_meta option")); 42 } 43 Ok(()) 44 }) 45 { 46 return e.to_compile_error().into(); 47 } 48 } 49 50 let repr_type = input.attrs.iter().find_map(|attr| { 51 if !attr.path().is_ident("repr") { 52 return None; 53 } 54 let mut found = None; 55 attr.parse_nested_meta(|it| { 56 found = it.path.get_ident().cloned(); 57 Ok(()) 58 }) 59 .unwrap(); 60 found 61 }); 62 63 let variants = if let Data::Enum(data) = &input.data { 64 &data.variants 65 } else { 66 return Error::new(input.ident.span(), "EnumMeta only supports enums") 67 .to_compile_error() 68 .into(); 69 }; 70 71 // Helper: extract the first string literal from a name-value attribute. 72 let extract_str_attr = |variant: &syn::Variant, ident: &str| -> Option<String> { 73 variant.attrs.iter().find_map(|a| { 74 if a.path().is_ident(ident) 75 && let Meta::NameValue(nv) = &a.meta 76 && let Expr::Lit(expr) = &nv.value 77 && let Lit::Str(s) = &expr.lit 78 { 79 Some(s.value()) 80 } else { 81 None 82 } 83 }) 84 }; 85 86 let mut entries = Vec::new(); 87 let mut description_arms = Vec::new(); 88 let mut code_arms = Vec::new(); 89 let mut from_str_arms = Vec::new(); 90 let mut as_ref_arms = Vec::new(); 91 let mut try_from_arms = Vec::new(); 92 93 for variant in variants { 94 let v_ident = &variant.ident; 95 let v_str = v_ident.to_string(); 96 97 if repr_type.is_some() { 98 if let Some((_, discriminant)) = &variant.discriminant { 99 try_from_arms.push(quote! { #discriminant => Ok(Self::#v_ident) }); 100 } else { 101 return Error::new(v_ident.span(), "missing discriminant expression") 102 .to_compile_error() 103 .into(); 104 }; 105 } 106 107 // Single pass: collect doc and code in one go, then use what's needed. 108 let doc = 109 enabled_doc.then(|| extract_str_attr(variant, "doc").map(|s| s.trim().to_string())); 110 let code = (enabled_domain_code).then(|| extract_str_attr(variant, "code")); 111 112 if let Some(doc) = doc { 113 let doc = match doc { 114 Some(d) => d, 115 None => { 116 return Error::new( 117 v_ident.span(), 118 format!("variant `{v_str}` is missing `/// documentation`"), 119 ) 120 .to_compile_error() 121 .into(); 122 } 123 }; 124 description_arms.push(quote! { Self::#v_ident => #doc }); 125 } 126 127 if let Some(code) = code { 128 let code = match code { 129 Some(c) => c, 130 None => { 131 return Error::new( 132 v_ident.span(), 133 format!("variant `{v_str}` is missing `#[code = \"...\"]`"), 134 ) 135 .to_compile_error() 136 .into(); 137 } 138 }; 139 from_str_arms.push(quote! { #code => Ok(Self::#v_ident) }); 140 code_arms.push(quote! { Self::#v_ident => #code }); 141 } else if enabled_str { 142 from_str_arms.push(quote! { #v_str => Ok(Self::#v_ident) }); 143 } 144 145 if enabled_str { 146 as_ref_arms.push(quote! { Self::#v_ident => #v_str }); 147 } 148 entries.push(quote! { Self::#v_ident, }); 149 } 150 151 let mut expanded = quote! { 152 impl #name { 153 /// Returns a slice of all enum variants. 154 pub const entries: &'static [Self] = &[#(#entries)*]; 155 } 156 }; 157 158 if enabled_doc { 159 expanded.extend(quote! { 160 impl #name { 161 /// Returns the documentation description associated 162 pub fn description(&self) -> &'static str { 163 match self { #(#description_arms),* } 164 } 165 } 166 }); 167 } 168 169 if enabled_domain_code { 170 expanded.extend(quote! { 171 impl #name { 172 /// Returns the domain code associated 173 pub fn code(&self) -> &'static str { 174 match self { #(#code_arms),* } 175 } 176 } 177 }); 178 } 179 180 if enabled_str { 181 expanded.extend(quote! { 182 impl AsRef<str> for #name { 183 fn as_ref(&self) -> &str { 184 match self { #(#as_ref_arms),* } 185 } 186 } 187 188 impl std::fmt::Display for #name { 189 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 190 f.write_str(self.as_ref()) 191 } 192 } 193 }); 194 } 195 196 if enabled_domain_code || enabled_str { 197 let unknown_label = if enabled_domain_code { "code" } else { "name" }; 198 expanded.extend(quote! { 199 impl std::str::FromStr for #name { 200 type Err = String; 201 fn from_str(s: &str) -> Result<Self, Self::Err> { 202 match s { 203 #(#from_str_arms,)* 204 _ => Err(format!("Unknown {0} for {1}: {2}", #unknown_label, stringify!(#name), s)) 205 } 206 } 207 } 208 }); 209 } 210 211 if let Some(repr) = repr_type { 212 expanded.extend(quote! { 213 impl TryFrom<#repr> for #name { 214 type Error = #repr; 215 fn try_from(value: #repr) -> Result<Self, Self::Error> { 216 match value { 217 #(#try_from_arms,)* 218 _ => Err(value), 219 } 220 } 221 } 222 }); 223 } 224 225 TokenStream::from(expanded) 226 }