Skip to main content

fatoora_derive/
lib.rs

1//! Proc-macro helpers for validating invoice inputs used by `fatoora-core`.
2//!
3//! # Examples
4//! ```rust
5//! use fatoora_derive::Validate;
6//!
7//! #[derive(Validate)]
8//! struct Payload {
9//!     #[validate(non_empty)]
10//!     name: String,
11//! }
12//! ```
13use proc_macro::TokenStream;
14use proc_macro2::TokenStream as TokenStream2;
15use quote::{ToTokens, quote};
16use syn::{Attribute, Data, DeriveInput, Fields, Type, parse_macro_input};
17
18mod rules;
19
20fn extract_error_type(attrs: &[Attribute]) -> TokenStream2 {
21    for attr in attrs.iter().filter(|a| a.path().is_ident("validate_error")) {
22        let mut ty = None;
23        attr.parse_nested_meta(|meta| {
24            ty = Some(meta.path.to_token_stream());
25            Ok(())
26        })
27        .unwrap();
28        if let Some(t) = ty {
29            return t;
30        }
31    }
32    quote! { String }
33}
34
35fn extract_rules(attrs: &[Attribute]) -> Vec<String> {
36    let mut out = vec![];
37    for attr in attrs.iter().filter(|a| a.path().is_ident("validate")) {
38        attr.parse_nested_meta(|meta| {
39            if let Some(id) = meta.path.get_ident() {
40                out.push(id.to_string());
41            }
42            Ok(())
43        })
44        .unwrap();
45    }
46    out
47}
48
49/// Only allow rules on String for now.
50fn is_string_type(ty: &Type) -> bool {
51    match ty {
52        Type::Path(p) => p
53            .path
54            .segments
55            .last()
56            .map(|s| s.ident == "String")
57            .unwrap_or(false),
58        Type::Reference(r) => {
59            if let Type::Path(p) = &*r.elem {
60                p.path
61                    .segments
62                    .last()
63                    .map(|s| s.ident == "String")
64                    .unwrap_or(false)
65            } else {
66                false
67            }
68        }
69        _ => false,
70    }
71}
72
73/// Derive constructor-based validation for structs.
74///
75/// # Errors
76/// Emits a compile error if rules are unknown or applied to unsupported field types.
77#[proc_macro_derive(Validate, attributes(validate, validate_error))]
78pub fn derive_validate(input: TokenStream) -> TokenStream {
79    let ast = parse_macro_input!(input as DeriveInput);
80    let struct_name = ast.ident;
81    let error_type = extract_error_type(&ast.attrs);
82    let struct_rules = extract_rules(&ast.attrs);
83
84    let mut ctor_params = vec![];
85    let mut ctor_assigns = vec![];
86    let mut validations = vec![];
87
88    let fields = match ast.data {
89        Data::Struct(s) => match s.fields {
90            Fields::Named(n) => n.named,
91            _ => return quote! { compile_error!("Validate supports named structs only"); }.into(),
92        },
93        _ => return quote! { compile_error!("Validate can only be used on structs"); }.into(),
94    };
95
96    for field in fields {
97        let ident = field.ident.unwrap();
98        let ty = field.ty;
99
100        ctor_params.push(quote! { #ident: #ty });
101        ctor_assigns.push(quote! { #ident });
102
103        let mut field_rules = extract_rules(&field.attrs);
104        if field_rules.iter().any(|r| r == "skip") {
105            continue;
106        }
107        if field_rules.is_empty() {
108            field_rules = struct_rules.clone();
109        }
110        if field_rules.is_empty() {
111            continue;
112        }
113
114        if !is_string_type(&ty) {
115            let msg = format!(
116                "Validation rules can only be applied to String fields: {}",
117                ident
118            );
119            return quote! { compile_error!(#msg); }.into();
120        }
121
122        for rule in field_rules {
123            let ts = match rules::dispatch(&rule, &ident) {
124                Some(ts) => ts,
125                None => {
126                    let msg = format!("Unknown rule `{}`", rule);
127                    return quote! { compile_error!(#msg); }.into();
128                }
129            };
130            validations.push(ts);
131        }
132    }
133
134    let out = quote! {
135        impl #struct_name {
136            #[allow(clippy::too_many_arguments)]
137            pub fn new(
138                #(#ctor_params),*
139            ) -> Result<Self, #error_type> {
140
141                type E = #error_type;
142
143                #(
144                    #validations
145                )*
146
147                Ok(Self {
148                    #(#ctor_assigns),*
149                })
150            }
151        }
152    };
153
154    out.into()
155}