taler-rust

GNU Taler code in Rust. Largely core banking integrations.
Log | Files | Refs | Submodules | README | LICENSE

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 }