diff --git a/packages/fortifier-macros/src/validate.rs b/packages/fortifier-macros/src/validate.rs index b13f029..7efed03 100644 --- a/packages/fortifier-macros/src/validate.rs +++ b/packages/fortifier-macros/src/validate.rs @@ -1,4 +1,5 @@ mod attributes; +mod data; mod r#enum; mod field; mod fields; @@ -7,33 +8,90 @@ mod r#type; mod r#union; use proc_macro2::TokenStream; -use quote::ToTokens; -use syn::{Data, DeriveInput, Result}; +use quote::{ToTokens, TokenStreamExt, quote}; +use syn::{DeriveInput, Generics, Ident, Result, Type, TypeTuple, punctuated::Punctuated}; -use crate::validate::{r#enum::ValidateEnum, r#struct::ValidateStruct, union::ValidateUnion}; +use crate::{validate::data::ValidateData, validation::Execution}; -pub enum Validate<'a> { - Struct(ValidateStruct<'a>), - Enum(ValidateEnum<'a>), - Union(ValidateUnion), +pub struct Validate<'a> { + ident: &'a Ident, + generics: &'a Generics, + context_type: Option, + data: ValidateData<'a>, } impl<'a> Validate<'a> { pub fn parse(input: &'a DeriveInput) -> Result { - Ok(match &input.data { - Data::Struct(data) => Self::Struct(ValidateStruct::parse(input, data)?), - Data::Enum(data) => Self::Enum(ValidateEnum::parse(input, data)?), - Data::Union(data) => Self::Union(ValidateUnion::parse(input, data)?), - }) + let mut result = Validate { + ident: &input.ident, + generics: &input.generics, + context_type: None, + data: ValidateData::parse(input)?, + }; + + for attribute in &input.attrs { + if !attribute.path().is_ident("validate") { + continue; + } + + attribute.parse_nested_meta(|meta| { + if meta.path.is_ident("context") { + result.context_type = Some(meta.value()?.parse()?); + + Ok(()) + } else { + Err(meta.error("unknown parameter")) + } + })?; + } + + Ok(result) } } impl<'a> ToTokens for Validate<'a> { fn to_tokens(&self, tokens: &mut TokenStream) { - match self { - Validate::Struct(r#struct) => r#struct.to_tokens(tokens), - Validate::Enum(r#enum) => r#enum.to_tokens(tokens), - Validate::Union(r#union) => r#union.to_tokens(tokens), - } + let ident = &self.ident; + let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + + let context_type = match &self.context_type { + Some(context_type) => context_type, + None => &Type::Tuple(TypeTuple { + paren_token: Default::default(), + elems: Punctuated::new(), + }), + }; + let (error_type, error_definition) = self.data.error_type(); + let sync_validations = self.data.validations(Execution::Sync); + let async_validations = self.data.validations(Execution::Async); + + let no_context_impl = self.context_type.is_none().then(|| { + quote! { + #[automatically_derived] + impl #impl_generics ::fortifier::Validate for #ident #type_generics #where_clause {} + } + }); + + tokens.append_all(quote! { + #error_definition + + #[automatically_derived] + impl #impl_generics ::fortifier::ValidateWithContext for #ident #type_generics #where_clause { + type Context = #context_type; + type Error = #error_type; + + fn validate_sync_with_context(&self, context: &Self::Context) -> Result<(), ::fortifier::ValidationErrors> { + #sync_validations + } + + fn validate_async_with_context(&self, context: &Self::Context) -> ::std::pin::Pin>>>> { + Box::pin(async move { + #async_validations + }) + } + } + + #no_context_impl + }) } } diff --git a/packages/fortifier-macros/src/validate/data.rs b/packages/fortifier-macros/src/validate/data.rs new file mode 100644 index 0000000..f5f369b --- /dev/null +++ b/packages/fortifier-macros/src/validate/data.rs @@ -0,0 +1,39 @@ +use proc_macro2::TokenStream; +use syn::{Data, DeriveInput, Result}; + +use crate::{ + validate::{r#enum::ValidateEnum, r#struct::ValidateStruct, union::ValidateUnion}, + validation::Execution, +}; + +pub enum ValidateData<'a> { + Struct(ValidateStruct<'a>), + Enum(ValidateEnum<'a>), + Union(ValidateUnion), +} + +impl<'a> ValidateData<'a> { + pub fn parse(input: &'a DeriveInput) -> Result { + Ok(match &input.data { + Data::Struct(data) => Self::Struct(ValidateStruct::parse(input, data)?), + Data::Enum(data) => Self::Enum(ValidateEnum::parse(input, data)?), + Data::Union(data) => Self::Union(ValidateUnion::parse(input, data)?), + }) + } + + pub fn error_type(&self) -> (TokenStream, TokenStream) { + match self { + ValidateData::Struct(r#struct) => r#struct.error_type(), + ValidateData::Enum(r#enum) => r#enum.error_type(), + ValidateData::Union(r#union) => r#union.error_type(), + } + } + + pub fn validations(&self, execution: Execution) -> 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), + } + } +} diff --git a/packages/fortifier-macros/src/validate/enum.rs b/packages/fortifier-macros/src/validate/enum.rs index a44b0b9..833bba4 100644 --- a/packages/fortifier-macros/src/validate/enum.rs +++ b/packages/fortifier-macros/src/validate/enum.rs @@ -1,6 +1,6 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, TokenStreamExt, format_ident, quote}; -use syn::{DataEnum, DeriveInput, Generics, Ident, Result, Variant, Visibility}; +use quote::{ToTokens, format_ident, quote}; +use syn::{DataEnum, DeriveInput, Ident, Result, Variant, Visibility}; use crate::{ validate::{ @@ -15,7 +15,6 @@ pub struct ValidateEnum<'a> { visibility: &'a Visibility, ident: &'a Ident, error_ident: Ident, - generics: &'a Generics, variants: Vec>, } @@ -25,7 +24,6 @@ impl<'a> ValidateEnum<'a> { visibility: &input.vis, ident: &input.ident, error_ident: format_ident!("{}ValidationError", input.ident), - generics: &input.generics, variants: Vec::with_capacity(data.variants.len()), }; @@ -41,7 +39,7 @@ impl<'a> ValidateEnum<'a> { Ok(result) } - fn error_type(&self) -> (&Ident, TokenStream) { + pub fn error_type(&self) -> (TokenStream, TokenStream) { let visibility = &self.visibility; let error_ident = &self.error_ident; @@ -51,14 +49,14 @@ impl<'a> ValidateEnum<'a> { .iter() .map(|variant| &variant.ident) .collect::>(); - let error_variant_types = self + let (error_variant_types, variant_error_types): (Vec<_>, Vec<_>) = self .variants .iter() - .map(|variant| variant.error_type().0) - .collect::>(); + .map(|variant| variant.error_type()) + .unzip(); ( - error_ident, + error_ident.to_token_stream(), quote! { #[allow(dead_code)] #[derive(Debug, PartialEq)] @@ -76,51 +74,23 @@ impl<'a> ValidateEnum<'a> { #[automatically_derived] impl ::std::error::Error for #error_ident {} + + #( #variant_error_types )* }, ) } -} - -impl<'a> ToTokens for ValidateEnum<'a> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let ident = &self.ident; - let (impl_generics, type_generics, where_clause) = &self.generics.split_for_impl(); - let (error_ident, error_type) = self.error_type(); - let variant_error_types = self.variants.iter().map(|variant| variant.error_type().1); - let sync_variant_match_arms = self - .variants - .iter() - .map(|variant| variant.match_arm(Execution::Sync)); - let async_variant_match_arms = self + pub fn validations(&self, execution: Execution) -> TokenStream { + let variant_match_arms = self .variants .iter() - .map(|variant| variant.match_arm(Execution::Async)); - - tokens.append_all(quote! { - #error_type + .map(|variant| variant.match_arm(execution)); - #( #variant_error_types )* - - #[automatically_derived] - impl #impl_generics ::fortifier::Validate for #ident #type_generics #where_clause { - type Error = #error_ident; - - fn validate_sync(&self) -> Result<(), ::fortifier::ValidationErrors> { - match &self { - #( #sync_variant_match_arms ),* - } - } - - fn validate_async(&self) -> ::std::pin::Pin>>>> { - Box::pin(async move { - match &self { - #( #async_variant_match_arms ),* - } - }) - } + quote! { + match &self { + #( #variant_match_arms ),* } - }) + } } } diff --git a/packages/fortifier-macros/src/validate/field.rs b/packages/fortifier-macros/src/validate/field.rs index 480d5f0..fd2f8ef 100644 --- a/packages/fortifier-macros/src/validate/field.rs +++ b/packages/fortifier-macros/src/validate/field.rs @@ -6,7 +6,7 @@ use syn::{Field, Ident, Result, Visibility}; use crate::{ validate::{attributes::enum_attributes, r#type::should_validate_type}, validation::{Execution, Validation}, - validations::{Custom, Email, Length, Regex, Url}, + validations::{Custom, Email, Length, Nested, Regex, Url}, }; pub enum LiteralOrIdent { @@ -96,8 +96,11 @@ impl<'a> ValidateField<'a> { } } - if !skip && should_validate_type(&field.ty) { + // 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 && result.validations.is_empty() && should_validate_type(&field.ty) { // TODO: Nested validation + result.validations.push(Box::new(Nested::new())); } Ok(result) diff --git a/packages/fortifier-macros/src/validate/struct.rs b/packages/fortifier-macros/src/validate/struct.rs index 6578a6c..99c7015 100644 --- a/packages/fortifier-macros/src/validate/struct.rs +++ b/packages/fortifier-macros/src/validate/struct.rs @@ -1,6 +1,5 @@ use proc_macro2::TokenStream; -use quote::{ToTokens, TokenStreamExt, quote}; -use syn::{DataStruct, DeriveInput, Generics, Ident, Result}; +use syn::{DataStruct, DeriveInput, Result}; use crate::{ validate::{field::ValidateFieldPrefix, fields::ValidateFields}, @@ -8,57 +7,24 @@ use crate::{ }; pub struct ValidateStruct<'a> { - ident: &'a Ident, - generics: &'a Generics, fields: ValidateFields<'a>, } impl<'a> ValidateStruct<'a> { pub fn parse(input: &'a DeriveInput, data: &'a DataStruct) -> Result { Ok(ValidateStruct { - ident: &input.ident, - generics: &input.generics, fields: ValidateFields::parse(&input.vis, input.ident.clone(), &data.fields)?, }) } -} -impl<'a> ToTokens for ValidateStruct<'a> { - fn to_tokens(&self, tokens: &mut TokenStream) { - let ident = &self.ident; - let (impl_generics, type_generics, where_clause) = self.generics.split_for_impl(); + pub fn error_type(&self) -> (TokenStream, TokenStream) { + self.fields.error_type() + } + pub fn validations(&self, execution: Execution) -> TokenStream { let error_wrapper = |tokens| tokens; - let (error_ident, error_type) = self.fields.error_type(); - let sync_validations = self.fields.validations( - Execution::Sync, - ValidateFieldPrefix::SelfKeyword, - &error_wrapper, - ); - let async_validations = self.fields.validations( - Execution::Async, - ValidateFieldPrefix::SelfKeyword, - &error_wrapper, - ); - - tokens.append_all(quote! { - #error_type - - #[automatically_derived] - impl #impl_generics ::fortifier::Validate for #ident #type_generics #where_clause { - type Error = #error_ident; - - fn validate_sync(&self) -> Result<(), ::fortifier::ValidationErrors> { - #sync_validations - } - - fn validate_async(&self) -> ::std::pin::Pin>>>> { - Box::pin(async { - #async_validations - }) - } - } - }) + self.fields + .validations(execution, ValidateFieldPrefix::SelfKeyword, &error_wrapper) } } diff --git a/packages/fortifier-macros/src/validate/union.rs b/packages/fortifier-macros/src/validate/union.rs index e7e4d35..a62b07d 100644 --- a/packages/fortifier-macros/src/validate/union.rs +++ b/packages/fortifier-macros/src/validate/union.rs @@ -1,17 +1,20 @@ use proc_macro2::TokenStream; -use quote::ToTokens; use syn::{DataUnion, DeriveInput, Result}; +use crate::validation::Execution; + pub struct ValidateUnion {} impl ValidateUnion { - pub fn parse(_input: &DeriveInput, _data: &DataUnion) -> Result { - todo!("union") + pub fn parse(input: &DeriveInput, _data: &DataUnion) -> Result { + Err(syn::Error::new_spanned(input, "union is not supported")) + } + + pub fn error_type(&self) -> (TokenStream, TokenStream) { + todo!() } -} -impl ToTokens for ValidateUnion { - fn to_tokens(&self, _tokens: &mut TokenStream) { - // TODO + pub fn validations(&self, _execution: Execution) -> TokenStream { + todo!() } } diff --git a/packages/fortifier-macros/src/validations.rs b/packages/fortifier-macros/src/validations.rs index af4f9e0..7264db6 100644 --- a/packages/fortifier-macros/src/validations.rs +++ b/packages/fortifier-macros/src/validations.rs @@ -1,11 +1,13 @@ mod custom; mod email; mod length; +mod nested; mod regex; mod url; pub use custom::*; pub use email::*; pub use length::*; +pub use nested::*; pub use regex::*; pub use url::*; diff --git a/packages/fortifier-macros/src/validations/nested.rs b/packages/fortifier-macros/src/validations/nested.rs new file mode 100644 index 0000000..987ff1c --- /dev/null +++ b/packages/fortifier-macros/src/validations/nested.rs @@ -0,0 +1,40 @@ +use proc_macro2::TokenStream; +use quote::{format_ident, quote}; +use syn::{Ident, Result, meta::ParseNestedMeta}; + +use crate::validation::{Execution, Validation}; + +#[derive(Default)] +pub struct Nested {} + +impl Nested { + pub fn new() -> Self { + Self {} + } +} + +impl Validation for Nested { + fn parse(_meta: &ParseNestedMeta<'_>) -> Result { + unimplemented!() + } + + fn ident(&self) -> Ident { + format_ident!("Nested") + } + + fn error_type(&self) -> TokenStream { + // TODO + quote!(::fortifier::NestedError) + } + + fn expr(&self, execution: Execution, expr: &TokenStream) -> Option { + match execution { + Execution::Sync => Some(quote! { + ::fortifier::ValidateWithContext::validate_sync_with_context(&#expr, context) + }), + Execution::Async => Some(quote! { + ::fortifier::ValidateWithContext::validate_async_with_context(&#expr, context).await + }), + } + } +} diff --git a/packages/fortifier-macros/tests/derive/context_pass.rs b/packages/fortifier-macros/tests/derive/context_pass.rs new file mode 100644 index 0000000..9823f56 --- /dev/null +++ b/packages/fortifier-macros/tests/derive/context_pass.rs @@ -0,0 +1,23 @@ +use fortifier::{Validate, ValidateWithContext, ValidationErrors}; + +struct Context { + min: usize, + max: usize, +} + +#[derive(Validate)] +#[validate(context = Context)] +struct CreateUser { + #[validate(length(min = context.min, max = context.max))] + name: String, +} + +fn main() -> Result<(), ValidationErrors> { + let data = CreateUser { + name: "John Doe".to_owned(), + }; + + data.validate_sync_with_context(&Context { min: 1, max: 256 })?; + + Ok(()) +} diff --git a/packages/fortifier/src/validate.rs b/packages/fortifier/src/validate.rs index b381796..7968683 100644 --- a/packages/fortifier/src/validate.rs +++ b/packages/fortifier/src/validate.rs @@ -30,29 +30,62 @@ impl From> for ValidationErrors { } } -/// Validate a schema. -pub trait Validate { +/// Validate a schema with context. +pub trait ValidateWithContext { + /// Validation context. + type Context: Send + Sync; + /// Validation error. type Error: Error; - /// Validate schema using all validators. - fn validate( + /// Validate schema using all validators with context. + fn validate_with_context( &self, + context: &Self::Context, ) -> Pin>> + Send>> where Self: Sync, { Box::pin(async { - self.validate_sync()?; - self.validate_async().await + self.validate_sync_with_context(context)?; + self.validate_async_with_context(context).await }) } + /// Validate schema using only synchronous validators with context. + fn validate_sync_with_context( + &self, + context: &Self::Context, + ) -> Result<(), ValidationErrors>; + + /// Validate schema using only asynchronous validators with context. + fn validate_async_with_context( + &self, + context: &Self::Context, + ) -> Pin>> + Send>>; +} + +/// Validate a schema. +pub trait Validate: ValidateWithContext { + /// Validate schema using all validators. + fn validate( + &self, + ) -> Pin::Error>>> + Send>> + where + Self: Sync, + { + self.validate_with_context(&()) + } + /// Validate schema using only synchronous validators. - fn validate_sync(&self) -> Result<(), ValidationErrors>; + fn validate_sync(&self) -> Result<(), ValidationErrors> { + self.validate_sync_with_context(&()) + } /// Validate schema using only asynchronous validators. fn validate_async( &self, - ) -> Pin>> + Send>>; + ) -> Pin>> + Send>> { + self.validate_async_with_context(&()) + } }