taler-rust

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

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 }