taler-rust

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

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 }