diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index f020073..970c160 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -1,20 +1,20 @@ use proc_macro2::TokenStream; use quote::quote; -use syn::{Field, Ident, Result}; +use syn::{Field, Result}; use crate::validations::{Email, Length}; pub struct ValidateField { - ident: Ident, + expr: TokenStream, // TODO: Consider using a trait for validations. email: Option, length: Option, } impl ValidateField { - pub fn parse(ident: Ident, field: &Field) -> Result { + pub fn parse(expr: TokenStream, field: &Field) -> Result { let mut result = Self { - ident, + expr, email: None, length: None, }; @@ -53,11 +53,8 @@ impl ValidateField { } pub fn sync_validations(&self) -> Vec { - let email = self.email.as_ref().map(|email| email.tokens(&self.ident)); - let length = self - .length - .as_ref() - .map(|length| length.tokens(&self.ident)); + let email = self.email.as_ref().map(|email| email.tokens(&self.expr)); + let length = self.length.as_ref().map(|length| length.tokens(&self.expr)); [email, length].into_iter().flatten().collect() } diff --git a/packages/fortifier-macros/src/validate/struct.rs b/packages/fortifier-macros/src/validate/struct.rs index e1f2097..939f884 100644 --- a/packages/fortifier-macros/src/validate/struct.rs +++ b/packages/fortifier-macros/src/validate/struct.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; use convert_case::{Case, Casing}; -use proc_macro2::TokenStream; +use proc_macro2::{Literal, TokenStream}; use quote::{ToTokens, TokenStreamExt, format_ident, quote}; use syn::{DataStruct, DeriveInput, Fields, FieldsNamed, FieldsUnnamed, Ident, Result}; @@ -54,10 +54,11 @@ impl ValidateNamedStruct { continue; }; - result.fields.insert( - field_ident.clone(), - ValidateField::parse(field_ident.clone(), field)?, - ); + let expr = quote!(self.#field_ident); + + result + .fields + .insert(field_ident.clone(), ValidateField::parse(expr, field)?); } Ok(result) @@ -147,26 +148,108 @@ impl ToTokens for ValidateNamedStruct { } pub struct ValidateUnnamedStruct { - #[allow(unused)] ident: Ident, - #[allow(unused)] + error_ident: Ident, fields: Vec, } impl ValidateUnnamedStruct { - fn parse(input: &DeriveInput, _data: &DataStruct, _fields: &FieldsUnnamed) -> Result { - let result = Self { + fn parse(input: &DeriveInput, _data: &DataStruct, fields: &FieldsUnnamed) -> Result { + let mut result = Self { ident: input.ident.clone(), + error_ident: format_ident!("{}ValidationError", input.ident), fields: Vec::default(), }; + for (index, field) in fields.unnamed.iter().enumerate() { + let index = Literal::usize_unsuffixed(index); + let expr = quote!(self.#index); + + result.fields.push(ValidateField::parse(expr, field)?); + } + Ok(result) } } impl ToTokens for ValidateUnnamedStruct { - fn to_tokens(&self, _tokens: &mut TokenStream) { - // TODO + fn to_tokens(&self, tokens: &mut TokenStream) { + let ident = &self.ident; + let error_ident = &self.error_ident; + let mut error_field_idents = vec![]; + let mut error_field_types = 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}"); + + error_field_idents.push(field_error_ident.clone()); + error_field_types.push(field.error_type()); + + for validation in field.sync_validations() { + sync_validations.push(quote! { + if let Err(err) = #validation { + errors.push(#error_ident::#field_error_ident(err)); + } + }); + } + + for validation in field.async_validations() { + async_validations.push(quote! { + if let Err(err) = #validation { + errors.push(#error_ident::#field_error_ident(err)); + } + }); + } + } + + tokens.append_all(quote! { + use fortifier::*; + + #[derive(Debug)] + enum #error_ident { + #( #error_field_idents(#error_field_types) ),* + } + + impl ::std::fmt::Display for #error_ident { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + write!(f, "{self:#?}") + } + } + + impl ::std::error::Error for #error_ident {} + + impl Validate for #ident { + type Error = #error_ident; + + fn validate_sync(&self) -> Result<(), ValidationErrors> { + let mut errors = vec![]; + + #(#sync_validations)* + + if !errors.is_empty() { + Err(errors.into()) + } else { + Ok(()) + } + } + + fn validate_async(&self) -> ::std::pin::Pin>>>> { + Box::pin(async { + let mut errors = vec![]; + + #(#async_validations)* + + if !errors.is_empty() { + Err(errors.into()) + } else { + Ok(()) + } + }) + } + } + }) } } @@ -187,8 +270,10 @@ impl ToTokens for ValidateUnitStruct { let ident = &self.ident; tokens.append_all(quote! { + use fortifier::ValidationErrors; + impl Validate for #ident { - type Error = Infallible; + type Error = ::std::convert::Infallible; fn validate_sync(&self) -> Result<(), ValidationErrors> { Ok(()) diff --git a/packages/fortifier-macros/src/validations/email.rs b/packages/fortifier-macros/src/validations/email.rs index c475a3e..47c476d 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::{Ident, Result, meta::ParseNestedMeta}; +use syn::{Result, meta::ParseNestedMeta}; #[derive(Default)] pub struct Email {} @@ -10,9 +10,9 @@ impl Email { Ok(Email::default()) } - pub fn tokens(&self, ident: &Ident) -> TokenStream { + pub fn tokens(&self, expr: &TokenStream) -> TokenStream { quote! { - self.#ident.validate_email() + #expr.validate_email() } } } diff --git a/packages/fortifier-macros/src/validations/length.rs b/packages/fortifier-macros/src/validations/length.rs index 38ee0d0..5799e68 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, Ident, Result, meta::ParseNestedMeta}; +use syn::{Expr, Result, meta::ParseNestedMeta}; #[derive(Default)] pub struct Length { @@ -37,7 +37,7 @@ impl Length { Ok(result) } - pub fn tokens(&self, ident: &Ident) -> TokenStream { + pub fn tokens(&self, expr: &TokenStream) -> TokenStream { let equal = if let Some(equal) = &self.equal { quote!(Some(#equal)) } else { @@ -55,7 +55,7 @@ impl Length { }; quote! { - self.#ident.validate_length(#equal, #min, #max) + #expr.validate_length(#equal, #min, #max) } } } diff --git a/packages/fortifier-macros/tests/derive/basic_pass.rs b/packages/fortifier-macros/tests/derive/struct_named_pass.rs similarity index 100% rename from packages/fortifier-macros/tests/derive/basic_pass.rs rename to packages/fortifier-macros/tests/derive/struct_named_pass.rs diff --git a/packages/fortifier-macros/tests/derive/struct_unit_pass.rs b/packages/fortifier-macros/tests/derive/struct_unit_pass.rs new file mode 100644 index 0000000..25200b3 --- /dev/null +++ b/packages/fortifier-macros/tests/derive/struct_unit_pass.rs @@ -0,0 +1,14 @@ +use std::error::Error; + +use fortifier::Validate; + +#[derive(Validate)] +struct CreateUser; + +fn main() -> Result<(), Box> { + let data = CreateUser; + + data.validate_sync()?; + + Ok(()) +} diff --git a/packages/fortifier-macros/tests/derive/struct_unnamed_pass.rs b/packages/fortifier-macros/tests/derive/struct_unnamed_pass.rs new file mode 100644 index 0000000..7cc79c9 --- /dev/null +++ b/packages/fortifier-macros/tests/derive/struct_unnamed_pass.rs @@ -0,0 +1,14 @@ +use std::error::Error; + +use fortifier::Validate; + +#[derive(Validate)] +struct CreateUser(#[validate(length(min = 1, max = 256))] String); + +fn main() -> Result<(), Box> { + let data = CreateUser("John Doe".to_owned()); + + data.validate_sync()?; + + Ok(()) +}