From d34f3c6c995b10599e499c0226fa30e4a7192186 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABlle=20Huisman?= Date: Sat, 6 Dec 2025 11:23:15 +0100 Subject: [PATCH] feat: merge field errors --- example/src/main.rs | 1 + .../fortifier-macros/src/validate/field.rs | 79 ++++++++++++++++--- .../fortifier-macros/src/validate/struct.rs | 44 +++++++++-- packages/fortifier-macros/src/validation.rs | 4 +- .../src/validations/custom.rs | 9 ++- .../fortifier-macros/src/validations/email.rs | 8 +- .../src/validations/length.rs | 8 +- .../fortifier-macros/src/validations/url.rs | 8 +- 8 files changed, 135 insertions(+), 26 deletions(-) diff --git a/example/src/main.rs b/example/src/main.rs index 312c56f..f70fdb1 100644 --- a/example/src/main.rs +++ b/example/src/main.rs @@ -14,6 +14,7 @@ struct CreateUser { url: String, #[validate(custom(function = validate_one_locale_required, error = OneLocaleRequiredError))] + #[validate(length(min = 1))] locales: Vec, } diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index 513f337..e7c4ffd 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -1,6 +1,7 @@ +use convert_case::{Case, Casing}; use proc_macro2::TokenStream; -use quote::quote; -use syn::{Field, Result}; +use quote::{ToTokens, format_ident, quote}; +use syn::{Field, Ident, Result}; use crate::{ validation::Validation, @@ -8,13 +9,23 @@ use crate::{ }; pub struct ValidateField { + error_type_ident: Ident, expr: TokenStream, validations: Vec>, } impl ValidateField { - pub fn parse(expr: TokenStream, field: &Field) -> Result { + pub fn parse( + type_prefix: &Ident, + ident: Ident, + expr: TokenStream, + field: &Field, + ) -> Result { + let error_ident = format_ident!("{}", ident.to_string().to_case(Case::UpperCamel)); + let error_type_ident = format_ident!("{type_prefix}{error_ident}ValidationError"); + let mut result = Self { + error_type_ident, expr, validations: vec![], }; @@ -48,28 +59,74 @@ impl ValidateField { Ok(result) } - pub fn error_type(&self) -> TokenStream { - // TODO: Merge error types + pub fn error_type( + &self, + ident: &Ident, + field_error_ident: &Ident, + ) -> (TokenStream, Option) { + if self.validations.len() > 1 { + let ident = format_ident!("{}{}ValidationError", ident, field_error_ident); + let variant_ident = self.validations.iter().map(|validation| validation.ident()); + let variant_type = self + .validations + .iter() + .map(|validation| validation.error_type()); - self.validations - .first() - .map(|validation| validation.error_type()) - .unwrap_or_else(|| quote!(())) + ( + ident.to_token_stream(), + Some(quote! { + #[derive(Debug)] + enum #ident { + #( #variant_ident(#variant_type) ),* + } + }), + ) + } else if let Some(validation) = self.validations.first() { + (validation.error_type(), None) + } else { + (quote!(()), None) + } } pub fn sync_validations(&self) -> Vec { + let error_type_ident = &self.error_type_ident; + self.validations .iter() .filter(|validation| !validation.is_async()) - .map(|validation| validation.tokens(&self.expr)) + .map(|validation| { + let validation_ident = validation.ident(); + let tokens = validation.tokens(&self.expr); + + if self.validations.len() > 1 { + quote! { + #tokens.map_err(#error_type_ident::#validation_ident) + } + } else { + tokens + } + }) .collect() } pub fn async_validations(&self) -> Vec { + let error_type_ident = &self.error_type_ident; + self.validations .iter() .filter(|validation| validation.is_async()) - .map(|validation| validation.tokens(&self.expr)) + .map(|validation| { + let validation_ident = validation.ident(); + let tokens = validation.tokens(&self.expr); + + if self.validations.len() > 1 { + quote! { + #tokens.map_err(#error_type_ident::#validation_ident) + } + } else { + tokens + } + }) .collect() } } diff --git a/packages/fortifier-macros/src/validate/struct.rs b/packages/fortifier-macros/src/validate/struct.rs index 939f884..f10309f 100644 --- a/packages/fortifier-macros/src/validate/struct.rs +++ b/packages/fortifier-macros/src/validate/struct.rs @@ -56,9 +56,10 @@ impl ValidateNamedStruct { let expr = quote!(self.#field_ident); - result - .fields - .insert(field_ident.clone(), ValidateField::parse(expr, field)?); + result.fields.insert( + field_ident.clone(), + ValidateField::parse(&input.ident, field_ident.clone(), expr, field)?, + ); } Ok(result) @@ -71,6 +72,7 @@ impl ToTokens for ValidateNamedStruct { let error_ident = &self.error_ident; let mut error_field_idents = vec![]; let mut error_field_types = vec![]; + let mut error_field_enums = vec![]; let mut sync_validations = vec![]; let mut async_validations = vec![]; @@ -78,8 +80,13 @@ impl ToTokens for ValidateNamedStruct { let field_error_ident = format_ident!("{}", &field_ident.to_string().to_case(Case::UpperCamel)); + let (error_type, error_enum) = field.error_type(ident, &field_error_ident); + error_field_idents.push(field_error_ident.clone()); - error_field_types.push(field.error_type()); + error_field_types.push(error_type); + if let Some(error_enum) = error_enum { + error_field_enums.push(error_enum); + } for validation in field.sync_validations() { sync_validations.push(quote! { @@ -101,19 +108,25 @@ impl ToTokens for ValidateNamedStruct { tokens.append_all(quote! { use fortifier::*; + #[allow(dead_code)] #[derive(Debug)] enum #error_ident { #( #error_field_idents(#error_field_types) ),* } + #[automatically_derived] impl ::std::fmt::Display for #error_ident { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { write!(f, "{self:#?}") } } + #[automatically_derived] impl ::std::error::Error for #error_ident {} + #(#error_field_enums)* + + #[automatically_derived] impl Validate for #ident { type Error = #error_ident; @@ -163,9 +176,15 @@ impl ValidateUnnamedStruct { for (index, field) in fields.unnamed.iter().enumerate() { let index = Literal::usize_unsuffixed(index); + let field_ident = format_ident!("F{index}"); let expr = quote!(self.#index); - result.fields.push(ValidateField::parse(expr, field)?); + result.fields.push(ValidateField::parse( + &input.ident, + field_ident, + expr, + field, + )?); } Ok(result) @@ -178,14 +197,20 @@ impl ToTokens for ValidateUnnamedStruct { let error_ident = &self.error_ident; let mut error_field_idents = vec![]; let mut error_field_types = vec![]; + let mut error_field_enums = vec![]; let mut sync_validations = vec![]; let mut async_validations = vec![]; for (index, field) in self.fields.iter().enumerate() { let field_error_ident = format_ident!("F{index}"); + let (error_type, error_enum) = field.error_type(ident, &field_error_ident); + error_field_idents.push(field_error_ident.clone()); - error_field_types.push(field.error_type()); + error_field_types.push(error_type); + if let Some(error_enum) = error_enum { + error_field_enums.push(error_enum); + } for validation in field.sync_validations() { sync_validations.push(quote! { @@ -207,19 +232,25 @@ impl ToTokens for ValidateUnnamedStruct { tokens.append_all(quote! { use fortifier::*; + #[allow(dead_code)] #[derive(Debug)] enum #error_ident { #( #error_field_idents(#error_field_types) ),* } + #[automatically_derived] impl ::std::fmt::Display for #error_ident { fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { write!(f, "{self:#?}") } } + #[automatically_derived] impl ::std::error::Error for #error_ident {} + #(#error_field_enums)* + + #[automatically_derived] impl Validate for #ident { type Error = #error_ident; @@ -272,6 +303,7 @@ impl ToTokens for ValidateUnitStruct { tokens.append_all(quote! { use fortifier::ValidationErrors; + #[automatically_derived] impl Validate for #ident { type Error = ::std::convert::Infallible; diff --git a/packages/fortifier-macros/src/validation.rs b/packages/fortifier-macros/src/validation.rs index b52c705..182b598 100644 --- a/packages/fortifier-macros/src/validation.rs +++ b/packages/fortifier-macros/src/validation.rs @@ -1,5 +1,5 @@ use proc_macro2::TokenStream; -use syn::{Result, meta::ParseNestedMeta}; +use syn::{Ident, Result, meta::ParseNestedMeta}; pub trait Validation { fn parse(_meta: &ParseNestedMeta<'_>) -> Result @@ -8,6 +8,8 @@ pub trait Validation { fn is_async(&self) -> bool; + fn ident(&self) -> Ident; + fn error_type(&self) -> TokenStream; fn tokens(&self, expr: &TokenStream) -> TokenStream; diff --git a/packages/fortifier-macros/src/validations/custom.rs b/packages/fortifier-macros/src/validations/custom.rs index aeeb779..e939d86 100644 --- a/packages/fortifier-macros/src/validations/custom.rs +++ b/packages/fortifier-macros/src/validations/custom.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, quote}; -use syn::{LitBool, Path, Result, Type, meta::ParseNestedMeta}; +use quote::{ToTokens, format_ident, quote}; +use syn::{Ident, LitBool, Path, Result, Type, meta::ParseNestedMeta}; use crate::validation::Validation; @@ -57,6 +57,11 @@ impl Validation for Custom { self.is_async } + fn ident(&self) -> Ident { + // TODO: Determine ident from function or error type. + format_ident!("Custom") + } + fn error_type(&self) -> TokenStream { self.error_type.to_token_stream() } diff --git a/packages/fortifier-macros/src/validations/email.rs b/packages/fortifier-macros/src/validations/email.rs index fd95f67..ee63a01 100644 --- a/packages/fortifier-macros/src/validations/email.rs +++ b/packages/fortifier-macros/src/validations/email.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; -use quote::quote; -use syn::{Result, meta::ParseNestedMeta}; +use quote::{format_ident, quote}; +use syn::{Ident, Result, meta::ParseNestedMeta}; use crate::validation::Validation; @@ -16,6 +16,10 @@ impl Validation for Email { false } + fn ident(&self) -> Ident { + format_ident!("Email") + } + fn error_type(&self) -> TokenStream { quote!(EmailError) } diff --git a/packages/fortifier-macros/src/validations/length.rs b/packages/fortifier-macros/src/validations/length.rs index 6883db9..d9e6b0b 100644 --- a/packages/fortifier-macros/src/validations/length.rs +++ b/packages/fortifier-macros/src/validations/length.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; -use quote::quote; -use syn::{Expr, Result, meta::ParseNestedMeta}; +use quote::{format_ident, quote}; +use syn::{Expr, Ident, Result, meta::ParseNestedMeta}; use crate::validation::Validation; @@ -43,6 +43,10 @@ impl Validation for Length { false } + fn ident(&self) -> Ident { + format_ident!("Length") + } + fn error_type(&self) -> TokenStream { quote!(LengthError) } diff --git a/packages/fortifier-macros/src/validations/url.rs b/packages/fortifier-macros/src/validations/url.rs index 093cbbe..d88875b 100644 --- a/packages/fortifier-macros/src/validations/url.rs +++ b/packages/fortifier-macros/src/validations/url.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; -use quote::quote; -use syn::{Result, meta::ParseNestedMeta}; +use quote::{format_ident, quote}; +use syn::{Ident, Result, meta::ParseNestedMeta}; use crate::validation::Validation; @@ -16,6 +16,10 @@ impl Validation for Url { false } + fn ident(&self) -> Ident { + format_ident!("Url") + } + fn error_type(&self) -> TokenStream { quote!(UrlError) }