lib.rs (6055B)
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 description_arms = Vec::new(); 74 let mut code_arms = Vec::new(); 75 let mut from_str_arms = Vec::new(); 76 let mut as_ref_arms = Vec::new(); 77 78 for variant in variants { 79 let v_ident = &variant.ident; 80 let v_str = v_ident.to_string(); 81 82 // Single pass: collect doc and code in one go, then use what's needed. 83 let doc = 84 enabled_doc.then(|| extract_str_attr(variant, "doc").map(|s| s.trim().to_string())); 85 let code = (enabled_code).then(|| extract_str_attr(variant, "code")); 86 87 if let Some(doc) = doc { 88 let doc = match doc { 89 Some(d) => d, 90 None => { 91 return Error::new( 92 v_ident.span(), 93 format!("variant `{v_str}` is missing `/// documentation`"), 94 ) 95 .to_compile_error() 96 .into(); 97 } 98 }; 99 description_arms.push(quote! { Self::#v_ident => #doc }); 100 } 101 102 if let Some(code) = code { 103 let code = match code { 104 Some(c) => c, 105 None => { 106 return Error::new( 107 v_ident.span(), 108 format!("variant `{v_str}` is missing `#[code = \"...\"]`"), 109 ) 110 .to_compile_error() 111 .into(); 112 } 113 }; 114 from_str_arms.push(quote! { #code => Ok(Self::#v_ident) }); 115 code_arms.push(quote! { Self::#v_ident => #code }); 116 } else if enabled_str { 117 from_str_arms.push(quote! { #v_str => Ok(Self::#v_ident) }); 118 } 119 120 if enabled_str { 121 as_ref_arms.push(quote! { Self::#v_ident => #v_str }); 122 } 123 } 124 125 let mut expanded = quote! {}; 126 127 if enabled_doc { 128 expanded.extend(quote! { 129 impl #name { 130 /// Returns the documentation description associated 131 pub fn description(&self) -> &'static str { 132 match self { #(#description_arms),* } 133 } 134 } 135 }); 136 } 137 138 if enabled_code { 139 expanded.extend(quote! { 140 impl #name { 141 /// Returns the domain code associated 142 pub fn code(&self) -> &'static str { 143 match self { #(#code_arms),* } 144 } 145 } 146 }); 147 } 148 149 if enabled_str { 150 expanded.extend(quote! { 151 impl AsRef<str> for #name { 152 fn as_ref(&self) -> &str { 153 match self { #(#as_ref_arms),* } 154 } 155 } 156 157 impl std::fmt::Display for #name { 158 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 159 f.write_str(self.as_ref()) 160 } 161 } 162 }); 163 } 164 165 if enabled_code || enabled_str { 166 let unknown_label = if enabled_code { "code" } else { "name" }; 167 expanded.extend(quote! { 168 impl std::str::FromStr for #name { 169 type Err = String; 170 fn from_str(s: &str) -> Result<Self, Self::Err> { 171 match s { 172 #(#from_str_arms,)* 173 _ => Err(format!("Unknown {0} for {1}: {2}", #unknown_label, stringify!(#name), s)) 174 } 175 } 176 } 177 }); 178 } 179 180 TokenStream::from(expanded) 181 }