lib.rs (6295B)
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_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_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 variants = if let Data::Enum(data) = &input.data { 51 &data.variants 52 } else { 53 return Error::new(input.ident.span(), "EnumMeta only supports enums") 54 .to_compile_error() 55 .into(); 56 }; 57 58 // Helper: extract the first string literal from a name-value attribute. 59 let extract_str_attr = |variant: &syn::Variant, ident: &str| -> Option<String> { 60 variant.attrs.iter().find_map(|a| { 61 if a.path().is_ident(ident) 62 && let Meta::NameValue(nv) = &a.meta 63 && let Expr::Lit(expr) = &nv.value 64 && let Lit::Str(s) = &expr.lit 65 { 66 Some(s.value()) 67 } else { 68 None 69 } 70 }) 71 }; 72 73 let mut entries = Vec::new(); 74 let mut description_arms = Vec::new(); 75 let mut code_arms = Vec::new(); 76 let mut from_str_arms = Vec::new(); 77 let mut as_ref_arms = Vec::new(); 78 79 for variant in variants { 80 let v_ident = &variant.ident; 81 let v_str = v_ident.to_string(); 82 83 // Single pass: collect doc and code in one go, then use what's needed. 84 let doc = 85 enabled_doc.then(|| extract_str_attr(variant, "doc").map(|s| s.trim().to_string())); 86 let code = (enabled_code).then(|| extract_str_attr(variant, "code")); 87 88 if let Some(doc) = doc { 89 let doc = match doc { 90 Some(d) => d, 91 None => { 92 return Error::new( 93 v_ident.span(), 94 format!("variant `{v_str}` is missing `/// documentation`"), 95 ) 96 .to_compile_error() 97 .into(); 98 } 99 }; 100 description_arms.push(quote! { Self::#v_ident => #doc }); 101 } 102 103 if let Some(code) = code { 104 let code = match code { 105 Some(c) => c, 106 None => { 107 return Error::new( 108 v_ident.span(), 109 format!("variant `{v_str}` is missing `#[code = \"...\"]`"), 110 ) 111 .to_compile_error() 112 .into(); 113 } 114 }; 115 from_str_arms.push(quote! { #code => Ok(Self::#v_ident) }); 116 code_arms.push(quote! { Self::#v_ident => #code }); 117 } else if enabled_str { 118 from_str_arms.push(quote! { #v_str => Ok(Self::#v_ident) }); 119 } 120 121 if enabled_str { 122 as_ref_arms.push(quote! { Self::#v_ident => #v_str }); 123 } 124 entries.push(quote! { Self::#v_ident, }); 125 } 126 127 let mut expanded = quote! { 128 impl #name { 129 /// Returns a slice of all enum variants. 130 pub const entries: &'static [Self] = &[#(#entries)*]; 131 } 132 }; 133 134 if enabled_doc { 135 expanded.extend(quote! { 136 impl #name { 137 /// Returns the documentation description associated 138 pub fn description(&self) -> &'static str { 139 match self { #(#description_arms),* } 140 } 141 } 142 }); 143 } 144 145 if enabled_code { 146 expanded.extend(quote! { 147 impl #name { 148 /// Returns the domain code associated 149 pub fn code(&self) -> &'static str { 150 match self { #(#code_arms),* } 151 } 152 } 153 }); 154 } 155 156 if enabled_str { 157 expanded.extend(quote! { 158 impl AsRef<str> for #name { 159 fn as_ref(&self) -> &str { 160 match self { #(#as_ref_arms),* } 161 } 162 } 163 164 impl std::fmt::Display for #name { 165 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 166 f.write_str(self.as_ref()) 167 } 168 } 169 }); 170 } 171 172 if enabled_code || enabled_str { 173 let unknown_label = if enabled_code { "code" } else { "name" }; 174 expanded.extend(quote! { 175 impl std::str::FromStr for #name { 176 type Err = String; 177 fn from_str(s: &str) -> Result<Self, Self::Err> { 178 match s { 179 #(#from_str_arms,)* 180 _ => Err(format!("Unknown {0} for {1}: {2}", #unknown_label, stringify!(#name), s)) 181 } 182 } 183 } 184 }); 185 } 186 187 TokenStream::from(expanded) 188 }