From 6382a1dc619b63a475eec4199a561e02850fce8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dani=C3=ABlle=20Huisman?= Date: Sun, 28 Dec 2025 20:32:14 +0100 Subject: [PATCH] feat: add top level validations --- examples/basic/src/email_address.rs | 8 + .../tests/validate/context_pass.rs | 13 +- .../tests/validate/enum_mixed_pass.rs | 9 + .../tests/validate/enum_named_pass.rs | 9 + .../tests/validate/enum_unit_pass.rs | 13 +- .../tests/validate/enum_unnamed_pass.rs | 9 + .../validate/struct_named_generics_pass.rs | 13 + .../validate/struct_named_lifetimes_pass.rs | 9 + .../tests/validate/struct_named_pass.rs | 9 + .../tests/validate/struct_unit_pass.rs | 13 +- .../validate/struct_unnamed_generics_pass.rs | 12 + .../validate/struct_unnamed_lifetimes_pass.rs | 9 + .../tests/validate/struct_unnamed_pass.rs | 9 + packages/fortifier-macros/src/validate.rs | 72 ++++- .../fortifier-macros/src/validate/data.rs | 43 ++- .../fortifier-macros/src/validate/enum.rs | 126 ++++---- .../fortifier-macros/src/validate/error.rs | 104 +++++++ .../fortifier-macros/src/validate/field.rs | 86 ++---- .../fortifier-macros/src/validate/fields.rs | 273 +++++++++++------- .../fortifier-macros/src/validate/struct.rs | 28 +- .../fortifier-macros/src/validate/type.rs | 2 +- .../fortifier-macros/src/validate/union.rs | 17 +- packages/fortifier-macros/src/validation.rs | 4 +- packages/fortifier-macros/src/validations.rs | 71 +++++ .../src/validations/custom.rs | 22 +- .../src/validations/email_address.rs | 4 +- .../src/validations/length.rs | 4 +- .../src/validations/nested.rs | 4 +- .../src/validations/phone_number.rs | 4 +- .../fortifier-macros/src/validations/range.rs | 6 +- .../fortifier-macros/src/validations/regex.rs | 4 +- .../fortifier-macros/src/validations/url.rs | 4 +- 32 files changed, 736 insertions(+), 277 deletions(-) create mode 100644 packages/fortifier-macros/src/validate/error.rs diff --git a/examples/basic/src/email_address.rs b/examples/basic/src/email_address.rs index b6052a0..a0500c7 100644 --- a/examples/basic/src/email_address.rs +++ b/examples/basic/src/email_address.rs @@ -1,6 +1,7 @@ use fortifier::Validate; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] pub enum ChangeEmailAddressRelation { Create { #[validate(email_address)] @@ -16,3 +17,10 @@ pub enum ChangeEmailAddressRelation { id: String, }, } + +#[derive(Debug, PartialEq)] +pub struct CustomError; + +fn validate_custom(_value: &ChangeEmailAddressRelation) -> Result<(), CustomError> { + Ok(()) +} diff --git a/packages/fortifier-macros-tests/tests/validate/context_pass.rs b/packages/fortifier-macros-tests/tests/validate/context_pass.rs index 9823f56..c66e6f7 100644 --- a/packages/fortifier-macros-tests/tests/validate/context_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/context_pass.rs @@ -1,4 +1,5 @@ use fortifier::{Validate, ValidateWithContext, ValidationErrors}; +use serde::{Deserialize, Serialize}; struct Context { min: usize, @@ -6,12 +7,22 @@ struct Context { } #[derive(Validate)] -#[validate(context = Context)] +#[validate( + context = Context, + custom(function = validate_custom, error = CustomError, context), +)] struct CreateUser { #[validate(length(min = context.min, max = context.max))] name: String, } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &CreateUser, _context: &Context) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = CreateUser { name: "John Doe".to_owned(), diff --git a/packages/fortifier-macros-tests/tests/validate/enum_mixed_pass.rs b/packages/fortifier-macros-tests/tests/validate/enum_mixed_pass.rs index ff2c938..21e8bdf 100644 --- a/packages/fortifier-macros-tests/tests/validate/enum_mixed_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/enum_mixed_pass.rs @@ -1,6 +1,8 @@ use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] enum FieldType { Boolean, Integer, @@ -11,6 +13,13 @@ enum FieldType { String(#[validate(range(min = 1))] usize), } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &FieldType) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = FieldType::Boolean; diff --git a/packages/fortifier-macros-tests/tests/validate/enum_named_pass.rs b/packages/fortifier-macros-tests/tests/validate/enum_named_pass.rs index 24d2fdc..0e7cffe 100644 --- a/packages/fortifier-macros-tests/tests/validate/enum_named_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/enum_named_pass.rs @@ -1,6 +1,8 @@ use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] enum ChangeEmailAddressRelation { Create { #[validate(email_address)] @@ -17,6 +19,13 @@ enum ChangeEmailAddressRelation { }, } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &ChangeEmailAddressRelation) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = ChangeEmailAddressRelation::Create { email_address: "john@doe.com".to_owned(), diff --git a/packages/fortifier-macros-tests/tests/validate/enum_unit_pass.rs b/packages/fortifier-macros-tests/tests/validate/enum_unit_pass.rs index 7ca7c56..f1c2175 100644 --- a/packages/fortifier-macros-tests/tests/validate/enum_unit_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/enum_unit_pass.rs @@ -1,15 +1,22 @@ -use std::convert::Infallible; - use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] enum ChangeEmailAddressRelation { Create, Update, Delete, } -fn main() -> Result<(), ValidationErrors> { +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &ChangeEmailAddressRelation) -> Result<(), CustomError> { + Ok(()) +} + +fn main() -> Result<(), ValidationErrors> { let data = ChangeEmailAddressRelation::Create; data.validate_sync()?; diff --git a/packages/fortifier-macros-tests/tests/validate/enum_unnamed_pass.rs b/packages/fortifier-macros-tests/tests/validate/enum_unnamed_pass.rs index 2f225ce..9051478 100644 --- a/packages/fortifier-macros-tests/tests/validate/enum_unnamed_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/enum_unnamed_pass.rs @@ -1,12 +1,21 @@ use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] enum ChangeEmailAddressRelation { Create(#[validate(email_address)] String), Update(String, #[validate(email_address)] String), Delete(String), } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &ChangeEmailAddressRelation) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = ChangeEmailAddressRelation::Create("john@doe.com".to_owned()); diff --git a/packages/fortifier-macros-tests/tests/validate/struct_named_generics_pass.rs b/packages/fortifier-macros-tests/tests/validate/struct_named_generics_pass.rs index 1c7b398..8284d88 100644 --- a/packages/fortifier-macros-tests/tests/validate/struct_named_generics_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/struct_named_generics_pass.rs @@ -1,6 +1,8 @@ use fortifier::{Validate, ValidateEmailAddress, ValidateLength, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] struct CreateUser> { #[validate(email_address)] email_address: E, @@ -9,6 +11,17 @@ struct CreateUser> { name: N, } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &CreateUser) -> Result<(), CustomError> +where + E: ValidateEmailAddress, + N: ValidateLength, +{ + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = CreateUser { email_address: "john@doe.com", diff --git a/packages/fortifier-macros-tests/tests/validate/struct_named_lifetimes_pass.rs b/packages/fortifier-macros-tests/tests/validate/struct_named_lifetimes_pass.rs index ae3615c..8942151 100644 --- a/packages/fortifier-macros-tests/tests/validate/struct_named_lifetimes_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/struct_named_lifetimes_pass.rs @@ -1,6 +1,8 @@ use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] struct CreateUser<'a, 'b> { #[validate(email_address)] email_address: &'a str, @@ -9,6 +11,13 @@ struct CreateUser<'a, 'b> { name: &'b str, } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom<'a, 'b>(_value: &CreateUser<'a, 'b>) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = CreateUser { email_address: "john@doe.com", diff --git a/packages/fortifier-macros-tests/tests/validate/struct_named_pass.rs b/packages/fortifier-macros-tests/tests/validate/struct_named_pass.rs index b162dee..2f05bc8 100644 --- a/packages/fortifier-macros-tests/tests/validate/struct_named_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/struct_named_pass.rs @@ -1,6 +1,8 @@ use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] struct CreateUser { #[validate(email_address)] email_address: String, @@ -9,6 +11,13 @@ struct CreateUser { name: String, } +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &CreateUser) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = CreateUser { email_address: "john@doe.com".to_owned(), diff --git a/packages/fortifier-macros-tests/tests/validate/struct_unit_pass.rs b/packages/fortifier-macros-tests/tests/validate/struct_unit_pass.rs index 7710ebf..76c1557 100644 --- a/packages/fortifier-macros-tests/tests/validate/struct_unit_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/struct_unit_pass.rs @@ -1,11 +1,18 @@ -use std::convert::Infallible; - use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] struct CreateUser; -fn main() -> Result<(), ValidationErrors> { +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &CreateUser) -> Result<(), CustomError> { + Ok(()) +} + +fn main() -> Result<(), ValidationErrors> { let data = CreateUser; data.validate_sync()?; diff --git a/packages/fortifier-macros-tests/tests/validate/struct_unnamed_generics_pass.rs b/packages/fortifier-macros-tests/tests/validate/struct_unnamed_generics_pass.rs index d5af8d0..25242f5 100644 --- a/packages/fortifier-macros-tests/tests/validate/struct_unnamed_generics_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/struct_unnamed_generics_pass.rs @@ -1,8 +1,20 @@ use fortifier::{Validate, ValidateLength, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] struct CreateUser>(#[validate(length(min = 1, max = 256))] N); +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &CreateUser) -> Result<(), CustomError> +where + N: ValidateLength, +{ + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = CreateUser("John Doe"); diff --git a/packages/fortifier-macros-tests/tests/validate/struct_unnamed_lifetimes_pass.rs b/packages/fortifier-macros-tests/tests/validate/struct_unnamed_lifetimes_pass.rs index 7e6afac..a764101 100644 --- a/packages/fortifier-macros-tests/tests/validate/struct_unnamed_lifetimes_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/struct_unnamed_lifetimes_pass.rs @@ -1,8 +1,17 @@ use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] struct CreateUser<'a>(#[validate(length(min = 1, max = 256))] &'a str); +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom<'a>(_value: &CreateUser<'a>) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = CreateUser("John Doe"); diff --git a/packages/fortifier-macros-tests/tests/validate/struct_unnamed_pass.rs b/packages/fortifier-macros-tests/tests/validate/struct_unnamed_pass.rs index 0a0e8ba..85b760a 100644 --- a/packages/fortifier-macros-tests/tests/validate/struct_unnamed_pass.rs +++ b/packages/fortifier-macros-tests/tests/validate/struct_unnamed_pass.rs @@ -1,8 +1,17 @@ use fortifier::{Validate, ValidationErrors}; +use serde::{Deserialize, Serialize}; #[derive(Validate)] +#[validate(custom(function = validate_custom, error = CustomError))] struct CreateUser(#[validate(length(min = 1, max = 256))] String); +#[derive(Debug, Deserialize, PartialEq, Serialize)] +struct CustomError; + +fn validate_custom(_value: &CreateUser) -> Result<(), CustomError> { + Ok(()) +} + fn main() -> Result<(), ValidationErrors> { let data = CreateUser("John Doe".to_owned()); diff --git a/packages/fortifier-macros/src/validate.rs b/packages/fortifier-macros/src/validate.rs index e99c8e4..ef55256 100644 --- a/packages/fortifier-macros/src/validate.rs +++ b/packages/fortifier-macros/src/validate.rs @@ -1,5 +1,6 @@ mod data; mod r#enum; +mod error; mod field; mod fields; mod r#struct; @@ -7,25 +8,42 @@ mod r#type; mod r#union; use proc_macro2::TokenStream; -use quote::{ToTokens, TokenStreamExt, quote}; -use syn::{DeriveInput, Generics, Ident, Result, Type, TypeTuple, punctuated::Punctuated}; - -use crate::{validate::data::ValidateData, validation::Execution}; +use quote::{ToTokens, TokenStreamExt, format_ident, quote}; +use syn::{ + DeriveInput, Generics, Ident, Result, Type, TypeNever, TypeTuple, Visibility, + punctuated::Punctuated, +}; + +use crate::{ + validate::{ + data::ValidateData, + error::{ErrorType, error_type, format_error_ident}, + }, + validation::{Execution, Validation}, + validations::Custom, +}; pub struct Validate<'a> { + visibility: &'a Visibility, ident: &'a Ident, generics: &'a Generics, + root_error_ident: Ident, context_type: Option, data: ValidateData<'a>, + validations: Vec>, } impl<'a> Validate<'a> { pub fn parse(input: &'a DeriveInput) -> Result { let mut result = Validate { + visibility: &input.vis, ident: &input.ident, generics: &input.generics, + // TODO: Make `Root` ident configurable to prevent collisions. + root_error_ident: format_ident!("Root"), context_type: None, data: ValidateData::parse(input)?, + validations: vec![], }; for attribute in &input.attrs { @@ -37,6 +55,16 @@ impl<'a> Validate<'a> { if meta.path.is_ident("context") { result.context_type = Some(meta.value()?.parse()?); + Ok(()) + } else if meta.path.is_ident("custom") { + result.validations.push(Box::new(Custom::parse( + // Type is never used in the custom validation, so pass an arbitrary value. + &Type::Never(TypeNever { + bang_token: Default::default(), + }), + &meta, + )?)); + Ok(()) } else { Err(meta.error("unknown parameter")) @@ -46,6 +74,26 @@ impl<'a> Validate<'a> { Ok(result) } + + fn error_type(&self) -> Option { + let root_error_type = error_type( + self.visibility, + self.ident, + &self.root_error_ident, + &self.validations, + ); + + self.data.error_type(root_error_type.as_ref()) + } + + fn validations(&self, execution: Execution) -> TokenStream { + self.data.validations( + execution, + &format_error_ident(self.ident), + &self.root_error_ident, + &self.validations, + ) + } } impl<'a> ToTokens for Validate<'a> { @@ -61,13 +109,17 @@ impl<'a> ToTokens for Validate<'a> { }), }; - let (error_type, error_definition) = self - .data - .error_type() - .unwrap_or_else(|| (quote!(::std::convert::Infallible), TokenStream::new())); + let (error_type, error_definition) = if let Some(ErrorType { + r#type, definition, .. + }) = self.error_type() + { + (r#type, definition) + } else { + (quote!(::std::convert::Infallible), None) + }; - let sync_validations = self.data.validations(Execution::Sync); - let async_validations = self.data.validations(Execution::Async); + let sync_validations = self.validations(Execution::Sync); + let async_validations = self.validations(Execution::Async); let no_context_impl = self.context_type.is_none().then(|| { quote! { diff --git a/packages/fortifier-macros/src/validate/data.rs b/packages/fortifier-macros/src/validate/data.rs index 4825b4c..85d91b0 100644 --- a/packages/fortifier-macros/src/validate/data.rs +++ b/packages/fortifier-macros/src/validate/data.rs @@ -1,9 +1,11 @@ use proc_macro2::TokenStream; -use syn::{Data, DeriveInput, Result}; +use syn::{Data, DeriveInput, Ident, Result}; use crate::{ - validate::{r#enum::ValidateEnum, r#struct::ValidateStruct, union::ValidateUnion}, - validation::Execution, + validate::{ + r#enum::ValidateEnum, error::ErrorType, r#struct::ValidateStruct, union::ValidateUnion, + }, + validation::{Execution, Validation}, }; pub enum ValidateData<'a> { @@ -21,19 +23,40 @@ impl<'a> ValidateData<'a> { }) } - pub fn error_type(&self) -> Option<(TokenStream, TokenStream)> { + pub fn error_type(&self, root_error_type: Option<&ErrorType>) -> Option { match self { - ValidateData::Struct(r#struct) => r#struct.error_type(), - ValidateData::Enum(r#enum) => r#enum.error_type(), + ValidateData::Struct(r#struct) => r#struct.error_type(root_error_type), + ValidateData::Enum(r#enum) => r#enum.error_type(root_error_type), ValidateData::Union(r#union) => r#union.error_type(), } } - pub fn validations(&self, execution: Execution) -> TokenStream { + pub fn validations( + &self, + execution: Execution, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], + ) -> TokenStream { match self { - ValidateData::Struct(r#struct) => r#struct.validations(execution), - ValidateData::Enum(r#enum) => r#enum.validations(execution), - ValidateData::Union(r#union) => r#union.validations(execution), + ValidateData::Struct(r#struct) => r#struct.validations( + execution, + root_type_prefix, + root_error_ident, + root_validations, + ), + ValidateData::Enum(r#enum) => r#enum.validations( + execution, + root_type_prefix, + root_error_ident, + root_validations, + ), + ValidateData::Union(r#union) => r#union.validations( + execution, + root_type_prefix, + root_error_ident, + root_validations, + ), } } } diff --git a/packages/fortifier-macros/src/validate/enum.rs b/packages/fortifier-macros/src/validate/enum.rs index 8f05794..7a199e4 100644 --- a/packages/fortifier-macros/src/validate/enum.rs +++ b/packages/fortifier-macros/src/validate/enum.rs @@ -1,14 +1,14 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, format_ident, quote}; +use quote::{format_ident, quote}; use syn::{DataEnum, DeriveInput, Ident, Result, Variant, Visibility}; use crate::{ - attributes::enum_attributes, validate::{ - field::{LiteralOrIdent, ValidateFieldPrefix, format_error_ident}, + error::{ErrorType, combined_error_type, format_error_ident}, + field::{LiteralOrIdent, ValidateFieldPrefix}, fields::ValidateFields, }, - validation::Execution, + validation::{Execution, Validation}, }; pub struct ValidateEnum<'a> { @@ -39,60 +39,57 @@ impl<'a> ValidateEnum<'a> { Ok(result) } - pub fn error_type(&self) -> Option<(TokenStream, TokenStream)> { + pub fn error_type(&self, root_error_type: Option<&ErrorType>) -> Option { if self.variants.is_empty() { return None; } - let visibility = &self.visibility; - let error_ident = &self.error_ident; - - let attributes = enum_attributes(); - let error_variant_idents = self + let variant_error_types = self .variants .iter() - .flat_map(|variant| variant.error_type().map(|_| &variant.ident)) + .flat_map(|variant| variant.error_type(root_error_type)) .collect::>(); - let (error_variant_types, variant_error_types): (Vec<_>, Vec<_>) = self - .variants - .iter() - .flat_map(|variant| variant.error_type()) - .unzip(); - if error_variant_types.is_empty() { + if variant_error_types.is_empty() { return None; } - Some(( - error_ident.to_token_stream(), - quote! { - #[allow(dead_code)] - #[derive(Debug, PartialEq)] - #attributes - #visibility enum #error_ident { - #( #error_variant_idents(#error_variant_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 {} - - #( #variant_error_types )* - }, + let variant_idents = variant_error_types + .iter() + .map(|ErrorType { variant_ident, .. }| variant_ident); + let variant_types = variant_error_types + .iter() + .map(|ErrorType { r#type, .. }| r#type); + let variant_definitions = variant_error_types + .iter() + .flat_map(|ErrorType { definition, .. }| definition); + + Some(combined_error_type( + self.visibility, + self.ident, + &self.error_ident, + variant_idents, + variant_types, + variant_definitions, + None, )) } - pub fn validations(&self, execution: Execution) -> TokenStream { - let variant_match_arms = self - .variants - .iter() - .map(|variant| variant.match_arm(execution)); + pub fn validations( + &self, + execution: Execution, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], + ) -> TokenStream { + let variant_match_arms = self.variants.iter().map(|variant| { + variant.match_arm( + execution, + root_type_prefix, + root_error_ident, + root_validations, + ) + }); quote! { match &self { @@ -130,11 +127,17 @@ impl<'a> ValidateEnumVariant<'a> { Ok(result) } - fn error_type(&self) -> Option<(TokenStream, TokenStream)> { - self.fields.error_type() + fn error_type(&self, root_error_type: Option<&ErrorType>) -> Option { + self.fields.error_type(Some(self.ident), root_error_type) } - fn match_arm(&self, exeuction: Execution) -> TokenStream { + fn match_arm( + &self, + exeuction: Execution, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], + ) -> TokenStream { let enum_ident = &self.enum_ident; let enum_error_ident = &self.enum_error_ident; let ident = &self.ident; @@ -144,8 +147,14 @@ impl<'a> ValidateEnumVariant<'a> { match &self.fields { ValidateFields::Named(fields) => { let field_idents = fields.idents(); - let validations = - fields.validations(exeuction, ValidateFieldPrefix::None, &error_wrapper); + let validations = fields.validations( + exeuction, + ValidateFieldPrefix::None, + &error_wrapper, + root_type_prefix, + root_error_ident, + root_validations, + ); // TODO: Only destructure fields required for validation. quote! { @@ -162,8 +171,14 @@ impl<'a> ValidateEnumVariant<'a> { LiteralOrIdent::Literal(literal) => format_ident!("f{literal}"), LiteralOrIdent::Ident(ident) => ident.clone(), }); - let validations = - fields.validations(exeuction, ValidateFieldPrefix::F, &error_wrapper); + let validations = fields.validations( + exeuction, + ValidateFieldPrefix::F, + &error_wrapper, + root_type_prefix, + root_error_ident, + root_validations, + ); quote! { #enum_ident::#ident( @@ -174,7 +189,14 @@ impl<'a> ValidateEnumVariant<'a> { } } ValidateFields::Unit(fields) => { - let validations = fields.validations(); + let validations = fields.validations( + exeuction, + ValidateFieldPrefix::None, + &error_wrapper, + root_type_prefix, + root_error_ident, + root_validations, + ); quote! { #enum_ident::#ident => { diff --git a/packages/fortifier-macros/src/validate/error.rs b/packages/fortifier-macros/src/validate/error.rs new file mode 100644 index 0000000..7a0c404 --- /dev/null +++ b/packages/fortifier-macros/src/validate/error.rs @@ -0,0 +1,104 @@ +use proc_macro2::TokenStream; +use quote::{ToTokens, format_ident, quote}; +use syn::{Ident, Visibility}; + +use crate::{attributes::enum_attributes, validation::Validation}; + +pub fn format_error_ident(ident: &Ident) -> Ident { + format_ident!("{}ValidationError", ident) +} + +pub fn format_error_ident_with_prefix(prefix: &Ident, ident: &Ident) -> Ident { + format_ident!("{}{}ValidationError", prefix, ident) +} + +pub struct ErrorType { + pub variant_ident: Ident, + pub r#type: TokenStream, + pub definition: Option, +} + +pub fn error_type( + visibility: &Visibility, + prefix: &Ident, + error_ident: &Ident, + validations: &[Box], +) -> Option { + if validations.len() > 1 { + let attributes = enum_attributes(); + let ident = format_error_ident_with_prefix(prefix, error_ident); + let variant_ident = validations.iter().map(|validation| validation.ident()); + let variant_type = validations.iter().map(|validation| validation.error_type()); + + Some(ErrorType { + variant_ident: error_ident.clone(), + r#type: ident.to_token_stream(), + definition: Some(quote! { + #[derive(Debug, PartialEq)] + #attributes + #visibility enum #ident { + #( #variant_ident(#variant_type) ),* + } + }), + }) + } else { + validations.first().map(|validation| ErrorType { + variant_ident: error_ident.clone(), + r#type: validation.error_type(), + definition: None, + }) + } +} + +pub fn combined_error_type<'a>( + visibility: &Visibility, + error_variant_ident: &Ident, + error_ident: &Ident, + error_field_idents: impl Iterator, + error_field_types: impl Iterator, + error_definitions: impl Iterator, + root_error_type: Option<&ErrorType>, +) -> ErrorType { + let attributes = enum_attributes(); + let root_error_field = root_error_type.as_ref().map( + |ErrorType { + variant_ident, + r#type, + .. + }| { + quote! { + #variant_ident(#r#type), + } + }, + ); + let root_error_definition = + root_error_type.and_then(|ErrorType { definition, .. }| definition.as_ref()); + + ErrorType { + variant_ident: error_variant_ident.clone(), + r#type: error_ident.to_token_stream(), + definition: Some(quote! { + #[allow(dead_code)] + #[derive(Debug, PartialEq)] + #attributes + #visibility enum #error_ident { + #root_error_field + #( #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 {} + + #root_error_definition + + #( #error_definitions )* + }), + } +} diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index fe3afa5..e8a4433 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -4,10 +4,14 @@ use quote::{ToTokens, format_ident, quote}; use syn::{Error, Field, Ident, Result, Visibility}; use crate::{ - attributes::enum_attributes, - validate::r#type::{KnownOrUnknown, should_validate_type}, + validate::{ + error::{ErrorType, error_type, format_error_ident_with_prefix}, + r#type::{KnownOrUnknown, should_validate_type}, + }, validation::{Execution, Validation}, - validations::{Custom, EmailAddress, Length, Nested, PhoneNumber, Range, Regex, Url}, + validations::{ + Custom, EmailAddress, Length, Nested, PhoneNumber, Range, Regex, Url, combine_validations, + }, }; pub enum LiteralOrIdent { @@ -67,48 +71,50 @@ impl<'a> ValidateField<'a> { if meta.path.is_ident("custom") { result .validations - .push(Box::new(Custom::parse(field, &meta)?)); + .push(Box::new(Custom::parse(&field.ty, &meta)?)); Ok(()) } else if meta.path.is_ident("email_address") { result .validations - .push(Box::new(EmailAddress::parse(field, &meta)?)); + .push(Box::new(EmailAddress::parse(&field.ty, &meta)?)); Ok(()) } else if meta.path.is_ident("length") { result .validations - .push(Box::new(Length::parse(field, &meta)?)); + .push(Box::new(Length::parse(&field.ty, &meta)?)); Ok(()) } else if meta.path.is_ident("nested") { result .validations - .push(Box::new(Nested::parse(field, &meta)?)); + .push(Box::new(Nested::parse(&field.ty, &meta)?)); skip_nested = true; Ok(()) } else if meta.path.is_ident("phone_number") { result .validations - .push(Box::new(PhoneNumber::parse(field, &meta)?)); + .push(Box::new(PhoneNumber::parse(&field.ty, &meta)?)); Ok(()) } else if meta.path.is_ident("range") { result .validations - .push(Box::new(Range::parse(field, &meta)?)); + .push(Box::new(Range::parse(&field.ty, &meta)?)); Ok(()) } else if meta.path.is_ident("regex") { result .validations - .push(Box::new(Regex::parse(field, &meta)?)); + .push(Box::new(Regex::parse(&field.ty, &meta)?)); Ok(()) } else if meta.path.is_ident("url") { - result.validations.push(Box::new(Url::parse(field, &meta)?)); + result + .validations + .push(Box::new(Url::parse(&field.ty, &meta)?)); Ok(()) } else if meta.path.is_ident("skip") { @@ -123,7 +129,6 @@ impl<'a> ValidateField<'a> { } // TODO: Use enum/struct generics to determine if a generic field type supports nested validation. - // TODO: Remove the validations empty check after resolving the issue above. if !skip_nested && result.validations.is_empty() && let Some(nested_type) = should_validate_type(&field.ty) @@ -149,32 +154,8 @@ impl<'a> ValidateField<'a> { &self.error_ident } - pub fn error_type(&self, ident: &Ident) -> Option<(TokenStream, Option)> { - if self.validations.len() > 1 { - let attributes = enum_attributes(); - let visibility = &self.visibility; - let ident = format_error_ident_with_prefix(ident, &self.error_ident); - let variant_ident = self.validations.iter().map(|validation| validation.ident()); - let variant_type = self - .validations - .iter() - .map(|validation| validation.error_type()); - - Some(( - ident.to_token_stream(), - Some(quote! { - #[derive(Debug, PartialEq)] - #attributes - #visibility enum #ident { - #( #variant_ident(#variant_type) ),* - } - }), - )) - } else { - self.validations - .first() - .map(|validation| (validation.error_type(), None)) - } + pub fn error_type(&self, ident: &Ident) -> Option { + error_type(self.visibility, ident, &self.error_ident, &self.validations) } pub fn validations( @@ -182,10 +163,9 @@ impl<'a> ValidateField<'a> { execution: Execution, field_prefix: ValidateFieldPrefix, ) -> Vec { - let error_type_ident = &self.error_type_ident; let ident = &self.ident; - let field_expr = match field_prefix { + let expr = match field_prefix { ValidateFieldPrefix::None => self.ident.to_token_stream(), ValidateFieldPrefix::SelfKeyword => quote!(self.#ident), ValidateFieldPrefix::F => match &self.ident { @@ -194,23 +174,7 @@ impl<'a> ValidateField<'a> { }, }; - self.validations - .iter() - .flat_map(|validation| { - let validation_ident = validation.ident(); - let expr = validation.expr(execution, &field_expr); - - expr.map(|expr| { - if self.validations.len() > 1 { - quote! { - #expr.map_err(#error_type_ident::#validation_ident) - } - } else { - expr - } - }) - }) - .collect() + combine_validations(execution, &self.error_type_ident, &expr, &self.validations) } } @@ -223,11 +187,3 @@ fn upper_camel_ident(ident: &Ident) -> Ident { format_ident!("{}", s.to_case(Case::UpperCamel)) } } - -pub fn format_error_ident(ident: &Ident) -> Ident { - format_ident!("{}ValidationError", ident) -} - -pub fn format_error_ident_with_prefix(prefix: &Ident, ident: &Ident) -> Ident { - format_ident!("{}{}ValidationError", prefix, ident) -} diff --git a/packages/fortifier-macros/src/validate/fields.rs b/packages/fortifier-macros/src/validate/fields.rs index a55d80a..764e616 100644 --- a/packages/fortifier-macros/src/validate/fields.rs +++ b/packages/fortifier-macros/src/validate/fields.rs @@ -1,17 +1,24 @@ +use std::iter::empty; + use proc_macro2::{Literal, TokenStream}; -use quote::{ToTokens, quote}; +use quote::quote; use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident, Result, Visibility}; use crate::{ - attributes::enum_attributes, - validate::field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix, format_error_ident}, - validation::Execution, + validate::{ + error::{ + ErrorType, combined_error_type, format_error_ident, format_error_ident_with_prefix, + }, + field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix}, + }, + validation::{Execution, Validation}, + validations::{combine_validations, combine_wrapped_validations, wrap_validations}, }; pub enum ValidateFields<'a> { Named(ValidateNamedFields<'a>), Unnamed(ValidateUnnamedFields<'a>), - Unit(ValidateUnitFields), + Unit(ValidateUnitFields<'a>), } impl<'a> ValidateFields<'a> { @@ -23,15 +30,21 @@ impl<'a> ValidateFields<'a> { Fields::Unnamed(fields) => { Self::Unnamed(ValidateUnnamedFields::parse(visibility, ident, fields)?) } - Fields::Unit => Self::Unit(ValidateUnitFields::parse()?), + Fields::Unit => Self::Unit(ValidateUnitFields::parse(visibility, ident)?), }) } - pub fn error_type(&self) -> Option<(TokenStream, TokenStream)> { + pub fn error_type( + &self, + error_variant_ident: Option<&Ident>, + root_error_type: Option<&ErrorType>, + ) -> Option { match self { - ValidateFields::Named(named) => named.error_type(), - ValidateFields::Unnamed(unnamed) => unnamed.error_type(), - ValidateFields::Unit(unit) => unit.error_type(), + ValidateFields::Named(named) => named.error_type(error_variant_ident, root_error_type), + ValidateFields::Unnamed(unnamed) => { + unnamed.error_type(error_variant_ident, root_error_type) + } + ValidateFields::Unit(unit) => unit.error_type(error_variant_ident, root_error_type), } } @@ -40,15 +53,35 @@ impl<'a> ValidateFields<'a> { execution: Execution, field_prefix: ValidateFieldPrefix, error_wrapper: &impl Fn(TokenStream) -> TokenStream, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], ) -> TokenStream { match self { - ValidateFields::Named(named) => { - named.validations(execution, field_prefix, error_wrapper) - } - ValidateFields::Unnamed(unnamed) => { - unnamed.validations(execution, field_prefix, error_wrapper) - } - ValidateFields::Unit(unit) => unit.validations(), + ValidateFields::Named(named) => named.validations( + execution, + field_prefix, + error_wrapper, + root_type_prefix, + root_error_ident, + root_validations, + ), + ValidateFields::Unnamed(unnamed) => unnamed.validations( + execution, + field_prefix, + error_wrapper, + root_type_prefix, + root_error_ident, + root_validations, + ), + ValidateFields::Unit(unit) => unit.validations( + execution, + field_prefix, + error_wrapper, + root_type_prefix, + root_error_ident, + root_validations, + ), } } } @@ -91,15 +124,21 @@ impl<'a> ValidateNamedFields<'a> { self.fields.iter().map(|field| field.ident()) } - fn error_type(&self) -> Option<(TokenStream, TokenStream)> { + fn error_type( + &self, + error_variant_ident: Option<&Ident>, + root_error_type: Option<&ErrorType>, + ) -> Option { if self.fields.is_empty() { None } else { Some(error_type( self.visibility, &self.ident, + error_variant_ident, &self.error_ident, self.fields.iter(), + root_error_type, )) } } @@ -109,6 +148,9 @@ impl<'a> ValidateNamedFields<'a> { execution: Execution, field_prefix: ValidateFieldPrefix, error_wrapper: &impl Fn(TokenStream) -> TokenStream, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], ) -> TokenStream { validations( execution, @@ -116,6 +158,9 @@ impl<'a> ValidateNamedFields<'a> { &self.error_ident, error_wrapper, self.fields.iter(), + root_type_prefix, + root_error_ident, + root_validations, ) } } @@ -154,15 +199,21 @@ impl<'a> ValidateUnnamedFields<'a> { self.fields.iter().map(|field| field.ident()) } - fn error_type(&self) -> Option<(TokenStream, TokenStream)> { + fn error_type( + &self, + error_variant_ident: Option<&Ident>, + root_error_type: Option<&ErrorType>, + ) -> Option { if self.fields.is_empty() { None } else { Some(error_type( self.visibility, &self.ident, + error_variant_ident, &self.error_ident, self.fields.iter(), + root_error_type, )) } } @@ -172,6 +223,9 @@ impl<'a> ValidateUnnamedFields<'a> { execution: Execution, field_prefix: ValidateFieldPrefix, error_wrapper: &impl Fn(TokenStream) -> TokenStream, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], ) -> TokenStream { validations( execution, @@ -179,24 +233,71 @@ impl<'a> ValidateUnnamedFields<'a> { &self.error_ident, error_wrapper, self.fields.iter(), + root_type_prefix, + root_error_ident, + root_validations, ) } } -pub struct ValidateUnitFields {} +pub struct ValidateUnitFields<'a> { + visibility: &'a Visibility, + ident: Ident, + error_ident: Ident, +} + +impl<'a> ValidateUnitFields<'a> { + fn parse(visibility: &'a Visibility, ident: Ident) -> Result { + let error_ident = format_error_ident(&ident); -impl ValidateUnitFields { - fn parse() -> Result { - Ok(Self {}) + Ok(Self { + visibility, + ident, + error_ident, + }) } - fn error_type(&self) -> Option<(TokenStream, TokenStream)> { - None + fn error_type( + &self, + error_variant_ident: Option<&Ident>, + root_error_type: Option<&ErrorType>, + ) -> Option { + if root_error_type.is_some() { + Some(error_type( + self.visibility, + &self.ident, + error_variant_ident, + &self.error_ident, + empty(), + root_error_type, + )) + } else { + None + } } - pub fn validations(&self) -> TokenStream { - quote! { - Ok(()) + pub fn validations( + &self, + execution: Execution, + field_prefix: ValidateFieldPrefix, + error_wrapper: &impl Fn(TokenStream) -> TokenStream, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], + ) -> TokenStream { + if root_validations.is_empty() { + quote!(Ok(())) + } else { + validations( + execution, + field_prefix, + &self.error_ident, + error_wrapper, + empty(), + root_type_prefix, + root_error_ident, + root_validations, + ) } } } @@ -204,96 +305,70 @@ impl ValidateUnitFields { fn error_type<'a>( visibility: &Visibility, ident: &Ident, + error_variant_ident: Option<&Ident>, error_ident: &Ident, fields: impl Iterator>, -) -> (TokenStream, TokenStream) { - let attributes = enum_attributes(); - - let mut error_field_idents = vec![]; - let mut error_field_types = vec![]; - let mut error_field_enums = vec![]; + root_error_type: Option<&ErrorType>, +) -> ErrorType { + let mut variant_idents = vec![]; + let mut variant_types = vec![]; + let mut definitions = vec![]; for field in fields { - if let Some((field_error_type, field_error_enum)) = field.error_type(ident) { - let field_error_ident = field.error_ident(); - - error_field_idents.push(field_error_ident); - error_field_types.push(field_error_type); - if let Some(error_enum) = field_error_enum { - error_field_enums.push(error_enum); + if let Some(ErrorType { + variant_ident, + r#type, + definition, + }) = field.error_type(ident) + { + variant_idents.push(variant_ident); + variant_types.push(r#type); + if let Some(definition) = definition { + definitions.push(definition); } } } - ( - error_ident.to_token_stream(), - quote! { - #[allow(dead_code)] - #[derive(Debug, PartialEq)] - #attributes - #visibility 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 )* - }, + combined_error_type( + visibility, + error_variant_ident.unwrap_or(ident), + error_ident, + variant_idents.iter(), + variant_types.iter(), + definitions.iter(), + root_error_type, ) } +#[expect(clippy::too_many_arguments)] fn validations<'a>( execution: Execution, field_prefix: ValidateFieldPrefix, error_ident: &Ident, error_wrapper: &impl Fn(TokenStream) -> TokenStream, fields: impl Iterator>, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], ) -> TokenStream { - let error_ident = &error_ident; - - let validations = fields - .flat_map(|field| { - let field_error_ident = field.error_ident(); - let validations = field.validations(execution, field_prefix); - - validations - .iter() - .map(|validation| { - let error = error_wrapper(quote!(#error_ident::#field_error_ident(err))); - - quote! { - if let Err(err) = #validation { - errors.push(#error); - } - } - }) - .collect::>() - }) - .collect::>(); + let root_validations = wrap_validations( + error_ident, + root_error_ident, + error_wrapper, + combine_validations( + execution, + &format_error_ident_with_prefix(root_type_prefix, root_error_ident), + "e!(self), + root_validations, + ), + ); - if validations.is_empty() { - quote! { - Ok(()) - } - } else { - quote! { - let mut errors = vec![]; + let validations = fields.flat_map(|field| { + let field_error_ident = field.error_ident(); + let validations = field.validations(execution, field_prefix); - #(#validations)* + wrap_validations(error_ident, field_error_ident, error_wrapper, validations) + }); - if !errors.is_empty() { - Err(errors.into()) - } else { - Ok(()) - } - } - } + combine_wrapped_validations(validations.chain(root_validations).collect::>()) } diff --git a/packages/fortifier-macros/src/validate/struct.rs b/packages/fortifier-macros/src/validate/struct.rs index 2b68277..1be43eb 100644 --- a/packages/fortifier-macros/src/validate/struct.rs +++ b/packages/fortifier-macros/src/validate/struct.rs @@ -1,9 +1,9 @@ use proc_macro2::TokenStream; -use syn::{DataStruct, DeriveInput, Result}; +use syn::{DataStruct, DeriveInput, Ident, Result}; use crate::{ - validate::{field::ValidateFieldPrefix, fields::ValidateFields}, - validation::Execution, + validate::{error::ErrorType, field::ValidateFieldPrefix, fields::ValidateFields}, + validation::{Execution, Validation}, }; pub struct ValidateStruct<'a> { @@ -17,14 +17,26 @@ impl<'a> ValidateStruct<'a> { }) } - pub fn error_type(&self) -> Option<(TokenStream, TokenStream)> { - self.fields.error_type() + pub fn error_type(&self, root_error_type: Option<&ErrorType>) -> Option { + self.fields.error_type(None, root_error_type) } - pub fn validations(&self, execution: Execution) -> TokenStream { + pub fn validations( + &self, + execution: Execution, + root_type_prefix: &Ident, + root_error_ident: &Ident, + root_validations: &[Box], + ) -> TokenStream { let error_wrapper = |tokens| tokens; - self.fields - .validations(execution, ValidateFieldPrefix::SelfKeyword, &error_wrapper) + self.fields.validations( + execution, + ValidateFieldPrefix::SelfKeyword, + &error_wrapper, + root_type_prefix, + root_error_ident, + root_validations, + ) } } diff --git a/packages/fortifier-macros/src/validate/type.rs b/packages/fortifier-macros/src/validate/type.rs index 4e461f4..ffaa0d7 100644 --- a/packages/fortifier-macros/src/validate/type.rs +++ b/packages/fortifier-macros/src/validate/type.rs @@ -2,7 +2,7 @@ use proc_macro2::TokenStream; use quote::{ToTokens, quote}; use syn::{GenericArgument, Path, PathArguments, Type, TypeParamBound}; -use crate::validate::field::format_error_ident; +use crate::validate::error::format_error_ident; const PRIMITIVE_AND_BUILT_IN_TYPES: [&str; 18] = [ "bool", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize", diff --git a/packages/fortifier-macros/src/validate/union.rs b/packages/fortifier-macros/src/validate/union.rs index ba001ab..2ba8ed3 100644 --- a/packages/fortifier-macros/src/validate/union.rs +++ b/packages/fortifier-macros/src/validate/union.rs @@ -1,7 +1,10 @@ use proc_macro2::TokenStream; -use syn::{DataUnion, DeriveInput, Result}; +use syn::{DataUnion, DeriveInput, Ident, Result}; -use crate::validation::Execution; +use crate::{ + validate::error::ErrorType, + validation::{Execution, Validation}, +}; pub struct ValidateUnion {} @@ -10,11 +13,17 @@ impl ValidateUnion { Err(syn::Error::new_spanned(input, "union is not supported")) } - pub fn error_type(&self) -> Option<(TokenStream, TokenStream)> { + pub fn error_type(&self) -> Option { todo!() } - pub fn validations(&self, _execution: Execution) -> TokenStream { + pub fn validations( + &self, + _execution: Execution, + _root_type_prefix: &Ident, + _root_error_ident: &Ident, + _root_validations: &[Box], + ) -> TokenStream { todo!() } } diff --git a/packages/fortifier-macros/src/validation.rs b/packages/fortifier-macros/src/validation.rs index 733bb2c..3e4a510 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::{Field, Ident, Result, meta::ParseNestedMeta}; +use syn::{Ident, Result, Type, meta::ParseNestedMeta}; #[derive(Clone, Copy)] pub enum Execution { @@ -8,7 +8,7 @@ pub enum Execution { } pub trait Validation { - fn parse(_field: &Field, _meta: &ParseNestedMeta<'_>) -> Result + fn parse(_type: &Type, _meta: &ParseNestedMeta<'_>) -> Result where Self: Sized; diff --git a/packages/fortifier-macros/src/validations.rs b/packages/fortifier-macros/src/validations.rs index 1136ee2..69cf84d 100644 --- a/packages/fortifier-macros/src/validations.rs +++ b/packages/fortifier-macros/src/validations.rs @@ -15,3 +15,74 @@ pub use phone_number::*; pub use range::*; pub use regex::*; pub use url::*; + +use proc_macro2::TokenStream; +use quote::quote; +use syn::Ident; + +use crate::validation::{Execution, Validation}; + +pub fn combine_validations( + execution: Execution, + error_type_ident: &Ident, + expr: &TokenStream, + validations: &[Box], +) -> Vec { + validations + .iter() + .flat_map(|validation| { + let validation_ident = validation.ident(); + let expr = validation.expr(execution, expr); + + expr.map(|expr| { + if validations.len() > 1 { + quote! { + #expr.map_err(#error_type_ident::#validation_ident) + } + } else { + expr + } + }) + }) + .collect() +} + +pub fn wrap_validations( + error_ident: &Ident, + error_field_ident: &Ident, + error_wrapper: &impl Fn(TokenStream) -> TokenStream, + validations: Vec, +) -> Vec { + validations + .iter() + .map(|validation| { + let error = error_wrapper(quote!(#error_ident::#error_field_ident(err))); + + quote! { + if let Err(err) = #validation { + errors.push(#error); + } + } + }) + .collect() +} + +pub fn combine_wrapped_validations(validations: Vec) -> TokenStream { + if validations.is_empty() { + quote! { + Ok(()) + } + } else { + quote! { + let mut errors = vec![]; + + #(#validations)* + + if !errors.is_empty() { + Err(errors.into()) + } else { + Ok(()) + } + } + } +} diff --git a/packages/fortifier-macros/src/validations/custom.rs b/packages/fortifier-macros/src/validations/custom.rs index caa0d12..4f50e15 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, format_ident, quote}; -use syn::{Field, Ident, LitBool, Path, Result, Type, meta::ParseNestedMeta}; +use syn::{Ident, LitBool, Path, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -8,13 +8,15 @@ pub struct Custom { execution: Execution, error_type: Type, function_path: Path, + context: bool, } impl Validation for Custom { - fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_type: &Type, meta: &ParseNestedMeta<'_>) -> Result { let mut execution = Execution::Sync; let mut error_type: Option = None; let mut function_path: Option = None; + let mut context = false; meta.parse_nested_meta(|meta| { if meta.path.is_ident("async") { @@ -29,6 +31,15 @@ impl Validation for Custom { execution = Execution::Async; } + Ok(()) + } else if meta.path.is_ident("context") { + if let Ok(value) = meta.value() { + let lit: LitBool = value.parse()?; + context = lit.value; + } else { + context = true; + } + Ok(()) } else if meta.path.is_ident("error") { error_type = Some(meta.value()?.parse()?); @@ -54,6 +65,7 @@ impl Validation for Custom { execution, error_type, function_path, + context, }) } @@ -67,19 +79,21 @@ impl Validation for Custom { } fn expr(&self, execution: Execution, expr: &TokenStream) -> Option { + let context_expr = self.context.then(|| quote!(, &context)); + match (execution, self.execution) { (Execution::Sync, Execution::Sync) => { let function_path = &self.function_path; Some(quote! { - #function_path(&#expr) + #function_path(&#expr #context_expr) }) } (Execution::Async, Execution::Async) => { let function_path = &self.function_path; Some(quote! { - #function_path(&#expr).await + #function_path(&#expr #context_expr).await }) } _ => None, diff --git a/packages/fortifier-macros/src/validations/email_address.rs b/packages/fortifier-macros/src/validations/email_address.rs index 8cb4d71..537d6d8 100644 --- a/packages/fortifier-macros/src/validations/email_address.rs +++ b/packages/fortifier-macros/src/validations/email_address.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Field, Ident, LitBool, LitInt, Result, meta::ParseNestedMeta}; +use syn::{Ident, LitBool, LitInt, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -21,7 +21,7 @@ impl Default for EmailAddress { } impl Validation for EmailAddress { - fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_type: &Type, meta: &ParseNestedMeta<'_>) -> Result { let mut result = EmailAddress::default(); if !meta.input.is_empty() { diff --git a/packages/fortifier-macros/src/validations/length.rs b/packages/fortifier-macros/src/validations/length.rs index b557c16..cc56af5 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::{format_ident, quote}; -use syn::{Expr, Field, Ident, Result, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -12,7 +12,7 @@ pub struct Length { } impl Validation for Length { - fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_type: &Type, meta: &ParseNestedMeta<'_>) -> Result { let mut result = Length::default(); meta.parse_nested_meta(|meta| { diff --git a/packages/fortifier-macros/src/validations/nested.rs b/packages/fortifier-macros/src/validations/nested.rs index 10020bd..52a26bb 100644 --- a/packages/fortifier-macros/src/validations/nested.rs +++ b/packages/fortifier-macros/src/validations/nested.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{ToTokens, format_ident, quote}; -use syn::{Field, Ident, Path, Result, meta::ParseNestedMeta}; +use syn::{Ident, Path, Result, Type, meta::ParseNestedMeta}; use crate::{ attributes::enum_field_attributes, @@ -19,7 +19,7 @@ impl Nested { } impl Validation for Nested { - fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_type: &Type, meta: &ParseNestedMeta<'_>) -> Result { let mut error_type: Option = None; meta.parse_nested_meta(|meta| { diff --git a/packages/fortifier-macros/src/validations/phone_number.rs b/packages/fortifier-macros/src/validations/phone_number.rs index 723ea51..da19a7c 100644 --- a/packages/fortifier-macros/src/validations/phone_number.rs +++ b/packages/fortifier-macros/src/validations/phone_number.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, Field, Ident, Result, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -11,7 +11,7 @@ pub struct PhoneNumber { } impl Validation for PhoneNumber { - fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_type: &Type, meta: &ParseNestedMeta<'_>) -> Result { let mut result = PhoneNumber::default(); if !meta.input.is_empty() { diff --git a/packages/fortifier-macros/src/validations/range.rs b/packages/fortifier-macros/src/validations/range.rs index 901ee7b..aaf5dc0 100644 --- a/packages/fortifier-macros/src/validations/range.rs +++ b/packages/fortifier-macros/src/validations/range.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, Field, Ident, Result, Type, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -13,9 +13,9 @@ pub struct Range { } impl Validation for Range { - fn parse(field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + fn parse(r#type: &Type, meta: &ParseNestedMeta<'_>) -> Result { let mut result = Range { - r#type: field.ty.clone(), + r#type: r#type.clone(), min: None, max: None, exclusive_min: None, diff --git a/packages/fortifier-macros/src/validations/regex.rs b/packages/fortifier-macros/src/validations/regex.rs index 5827d02..1c0c57b 100644 --- a/packages/fortifier-macros/src/validations/regex.rs +++ b/packages/fortifier-macros/src/validations/regex.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; use quote::{format_ident, quote}; -use syn::{Expr, Field, Ident, Result, meta::ParseNestedMeta}; +use syn::{Expr, Ident, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -9,7 +9,7 @@ pub struct Regex { } impl Validation for Regex { - fn parse(_field: &Field, meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_type: &Type, meta: &ParseNestedMeta<'_>) -> Result { let mut expression: Option = None; if let Ok(value) = meta.value() { diff --git a/packages/fortifier-macros/src/validations/url.rs b/packages/fortifier-macros/src/validations/url.rs index f6f9587..4b1b353 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::{format_ident, quote}; -use syn::{Field, Ident, Result, meta::ParseNestedMeta}; +use syn::{Ident, Result, Type, meta::ParseNestedMeta}; use crate::validation::{Execution, Validation}; @@ -8,7 +8,7 @@ use crate::validation::{Execution, Validation}; pub struct Url {} impl Validation for Url { - fn parse(_field: &Field, _meta: &ParseNestedMeta<'_>) -> Result { + fn parse(_type: &Type, _meta: &ParseNestedMeta<'_>) -> Result { Ok(Url::default()) }