diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 463ec7f2..1e9b5a84 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -80,8 +80,11 @@ pub use validation::regex::ValidateRegex; pub use validation::required::ValidateRequired; pub use validation::urls::ValidateUrl; -pub use traits::{Validate, ValidateArgs}; -pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind}; +pub use traits::{Constraints, Validate, ValidateArgs}; +pub use types::{ + LengthConstraint, ValidationConstraint, ValidationConstraints, ValidationConstraintsKind, + ValidationError, ValidationErrors, ValidationErrorsKind, +}; #[cfg(feature = "derive")] pub use validator_derive::Validate; diff --git a/validator/src/traits.rs b/validator/src/traits.rs index 790ed26a..10ca5735 100644 --- a/validator/src/traits.rs +++ b/validator/src/traits.rs @@ -1,6 +1,12 @@ -use crate::types::{ValidationErrors, ValidationErrorsKind}; -use std::collections::btree_map::BTreeMap; -use std::collections::HashMap; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; + +#[cfg(feature = "indexmap")] +use indexmap::{IndexMap, IndexSet}; + +use crate::{ + types::{ValidationErrors, ValidationErrorsKind}, + ValidationConstraints, +}; /// This is the original trait that was implemented by deriving `Validate`. It will still be /// implemented for struct validations that don't take custom arguments. The call is being @@ -135,3 +141,111 @@ where } } } + +pub trait Constraints { + fn constraints() -> ValidationConstraints; + + fn is_collection() -> bool { + false + } +} + +impl Constraints for &T { + fn constraints() -> ValidationConstraints { + T::constraints() + } + + fn is_collection() -> bool { + T::is_collection() + } +} + +impl Constraints for Option { + fn constraints() -> ValidationConstraints { + T::constraints() + } + + fn is_collection() -> bool { + T::is_collection() + } +} + +macro_rules! impl_constraints { + ($ty:ty) => { + impl Constraints for $ty { + fn constraints() -> ValidationConstraints { + T::constraints() + } + + fn is_collection() -> bool { + true + } + } + }; +} + +impl_constraints!(&[T]); +impl_constraints!(Vec); +impl_constraints!(BTreeSet); + +impl Constraints for [T; N] { + fn constraints() -> ValidationConstraints { + T::constraints() + } + + fn is_collection() -> bool { + true + } +} + +impl Constraints for HashMap { + fn constraints() -> ValidationConstraints { + V::constraints() + } + + fn is_collection() -> bool { + true + } +} + +impl Constraints for HashSet { + fn constraints() -> ValidationConstraints { + V::constraints() + } + + fn is_collection() -> bool { + true + } +} + +impl Constraints for BTreeMap { + fn constraints() -> ValidationConstraints { + V::constraints() + } + + fn is_collection() -> bool { + true + } +} + +#[cfg(feature = "indexmap")] +impl Constraints for IndexMap { + fn constraints() -> ValidationConstraints { + V::constraints() + } + + fn is_collection() -> bool { + true + } +} + +#[cfg(feature = "indexmap")] +impl Constraints for IndexSet { + fn constraints() -> ValidationConstraints { + V::constraints() + } + + fn is_collection() -> bool { + true + } +} diff --git a/validator/src/types.rs b/validator/src/types.rs index 861f2d28..d716b7af 100644 --- a/validator/src/types.rs +++ b/validator/src/types.rs @@ -204,3 +204,137 @@ impl std::error::Error for ValidationErrors { None } } + +#[derive(Debug, Clone, PartialEq, Eq, Serialize)] +pub enum LengthConstraint { + Range { min: Option, max: Option }, + Equal(u64), +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[non_exhaustive] +pub enum ValidationConstraint { + CreditCard { + code: Cow<'static, str>, + }, + Contains { + code: Cow<'static, str>, + pattern: Cow<'static, str>, + }, + DoesNotContain { + code: Cow<'static, str>, + pattern: Cow<'static, str>, + }, + Email { + code: Cow<'static, str>, + }, + Ip { + code: Cow<'static, str>, + v4: bool, + v6: bool, + }, + Length { + code: Cow<'static, str>, + length: LengthConstraint, + }, + MustMatch { + code: Cow<'static, str>, + other: Cow<'static, str>, + }, + Nested, + NonControlCharacter { + code: Cow<'static, str>, + }, + Range { + code: Cow<'static, str>, + min: Option>, + max: Option>, + exclusive_min: Option>, + exclusive_max: Option>, + }, + Regex { + code: Cow<'static, str>, + path: Cow<'static, str>, + }, + Required { + code: Cow<'static, str>, + }, + RequiredNested { + code: Cow<'static, str>, + }, + Url { + code: Cow<'static, str>, + }, + Custom { + code: Option>, + function: Cow<'static, str>, + }, +} + +impl ValidationConstraint { + pub fn code(&self) -> &str { + match self { + Self::CreditCard { code, .. } => &code, + Self::Contains { code, .. } => &code, + Self::Custom { code, .. } => code.as_deref().unwrap_or("custom"), + Self::DoesNotContain { code, .. } => &code, + Self::Email { code, .. } => &code, + Self::Ip { code, .. } => &code, + Self::Length { code, .. } => &code, + Self::MustMatch { code, .. } => &code, + Self::Nested => "nested", + Self::NonControlCharacter { code, .. } => &code, + Self::Range { code, .. } => &code, + Self::Regex { code, .. } => &code, + Self::Required { code, .. } => &code, + Self::RequiredNested { code, .. } => &code, + Self::Url { code, .. } => &code, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ValidationConstraintsKind { + Struct(Box), + Collection(Box), + Field(Vec), +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize)] +pub struct ValidationConstraints(pub HashMap, Vec>); + +impl ValidationConstraints { + pub fn add(&mut self, field: impl Into>, constraint: ValidationConstraint) { + let entry = self.0.entry(field.into()).or_default(); + + match entry.iter_mut().find_map(|kind| match kind { + ValidationConstraintsKind::Field(field) => Some(field), + _ => None, + }) { + Some(field) => field.push(constraint), + None => entry.push(ValidationConstraintsKind::Field(vec![constraint])), + } + } + + pub fn merge( + &mut self, + field: impl Into>, + child: ValidationConstraints, + is_collection: bool, + ) { + if is_collection { + self.add_nested(field.into(), ValidationConstraintsKind::Collection(Box::new(child))); + } else { + self.add_nested(field.into(), ValidationConstraintsKind::Struct(Box::new(child))); + } + } + + fn add_nested( + &mut self, + field: impl Into>, + constraints: ValidationConstraintsKind, + ) { + self.0.entry(field.into()).or_default().push(constraints); + } +} diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index a10e9c36..f17eba4a 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -1,25 +1,10 @@ -use darling::ast::Data; -use darling::util::{Override, WithOriginal}; use darling::FromDeriveInput; +use darling::{ast::Data, util::WithOriginal}; use proc_macro_error::{abort, proc_macro_error}; -use quote::{quote, ToTokens}; +use quote::quote; use syn::{parse_macro_input, DeriveInput, Field, GenericParam, Path, PathArguments}; -use tokens::cards::credit_card_tokens; -use tokens::contains::contains_tokens; -use tokens::custom::custom_tokens; -use tokens::does_not_contain::does_not_contain_tokens; -use tokens::email::email_tokens; -use tokens::ip::ip_tokens; -use tokens::length::length_tokens; -use tokens::must_match::must_match_tokens; -use tokens::nested::nested_tokens; -use tokens::non_control_character::non_control_char_tokens; -use tokens::range::range_tokens; -use tokens::regex::regex_tokens; -use tokens::required::required_tokens; use tokens::schema::schema_tokens; -use tokens::url::url_tokens; use types::*; use utils::quote_use_stmts; @@ -27,200 +12,6 @@ mod tokens; mod types; mod utils; -impl ToTokens for ValidateField { - fn to_tokens(&self, tokens: &mut proc_macro2::TokenStream) { - let field_name = self.ident.clone().unwrap(); - let field_name_str = self.ident.clone().unwrap().to_string(); - - let type_name = self.ty.to_token_stream().to_string(); - let is_number = NUMBER_TYPES.contains(&type_name); - - let (actual_field, wrapper_closure) = self.if_let_option_wrapper(&field_name, is_number); - - // Length validation - let length = if let Some(length) = self.length.clone() { - wrapper_closure(length_tokens(length, &actual_field, &field_name_str)) - } else { - quote!() - }; - - // Email validation - let email = if let Some(email) = self.email.clone() { - wrapper_closure(email_tokens( - match email { - Override::Inherit => Email::default(), - Override::Explicit(e) => e, - }, - &actual_field, - &field_name_str, - )) - } else { - quote!() - }; - - // Credit card validation - let card = if let Some(credit_card) = self.credit_card.clone() { - wrapper_closure(credit_card_tokens( - match credit_card { - Override::Inherit => Card::default(), - Override::Explicit(c) => c, - }, - &actual_field, - &field_name_str, - )) - } else { - quote!() - }; - - // Url validation - let url = if let Some(url) = self.url.clone() { - wrapper_closure(url_tokens( - match url { - Override::Inherit => Url::default(), - Override::Explicit(u) => u, - }, - &actual_field, - &field_name_str, - )) - } else { - quote!() - }; - - // Ip address validation - let ip = if let Some(ip) = self.ip.clone() { - wrapper_closure(ip_tokens( - match ip { - Override::Inherit => Ip::default(), - Override::Explicit(i) => i, - }, - &actual_field, - &field_name_str, - )) - } else { - quote!() - }; - - // Non control character validation - let ncc = if let Some(ncc) = self.non_control_character.clone() { - wrapper_closure(non_control_char_tokens( - match ncc { - Override::Inherit => NonControlCharacter::default(), - Override::Explicit(n) => n, - }, - &actual_field, - &field_name_str, - )) - } else { - quote!() - }; - - // Range validation - let range = if let Some(range) = self.range.clone() { - wrapper_closure(range_tokens(range, &actual_field, &field_name_str)) - } else { - quote!() - }; - - // Required validation - let required = if let Some(required) = self.required.clone() { - required_tokens( - match required { - Override::Inherit => Required::default(), - Override::Explicit(r) => r, - }, - &field_name, - &field_name_str, - ) - } else { - quote!() - }; - - // Contains validation - let contains = if let Some(contains) = self.contains.clone() { - wrapper_closure(contains_tokens(contains, &actual_field, &field_name_str)) - } else { - quote!() - }; - - // Does not contain validation - let does_not_contain = if let Some(does_not_contain) = self.does_not_contain.clone() { - wrapper_closure(does_not_contain_tokens( - does_not_contain, - &actual_field, - &field_name_str, - )) - } else { - quote!() - }; - - // Must match validation - let must_match = if let Some(must_match) = self.must_match.clone() { - // TODO: handle option for other - wrapper_closure(must_match_tokens(must_match, &actual_field, &field_name_str)) - } else { - quote!() - }; - - // Regex validation - let regex = if let Some(regex) = self.regex.clone() { - wrapper_closure(regex_tokens(regex, &actual_field, &field_name_str)) - } else { - quote!() - }; - - // Custom validation - let mut custom = quote!(); - // We try to be smart when passing arguments - let is_cow = type_name.contains("Cow <"); - let custom_actual_field = if is_cow { - quote!(#actual_field.as_ref()) - } else if is_number || type_name.starts_with("&") { - quote!(#actual_field) - } else { - quote!(&#actual_field) - }; - - for c in &self.custom { - let tokens = custom_tokens(c.clone(), &custom_actual_field, &field_name_str); - custom = quote!( - #custom - - #tokens - ); - } - if !self.custom.is_empty() { - custom = wrapper_closure(custom); - } - - let nested = if let Some(n) = self.nested { - if n { - wrapper_closure(nested_tokens(&actual_field, &field_name_str)) - } else { - quote!() - } - } else { - quote!() - }; - - tokens.extend(quote! { - #length - #email - #card - #url - #ip - #ncc - #range - #required - #contains - #does_not_contain - #must_match - #regex - #custom - #nested - }); - } -} - // The main struct we get from parsing the attributes // The "supports(struct_named)" attribute guarantees only named structs to work with this macro #[derive(Debug, FromDeriveInput)] @@ -381,6 +172,9 @@ pub fn derive_validation(input: proc_macro::TokenStream) -> proc_macro::TokenStr quote!() }; + let (validation_fields, constraints): (Vec<_>, Vec<_>) = + validation_fields.into_iter().map(ValidateField::into_tokens).unzip(); + quote!( #argless_validation @@ -405,6 +199,17 @@ pub fn derive_validation(input: proc_macro::TokenStream) -> proc_macro::TokenStr } } } + + impl #imp ::validator::Constraints for #ident #ty #whr { + fn constraints() -> ::validator::ValidationConstraints { + let mut constraints = ::validator::ValidationConstraints::default(); + + #(#constraints)* + + constraints + } + } + ) .into() } diff --git a/validator_derive/src/tokens/cards.rs b/validator_derive/src/tokens/cards.rs index 288efc93..a678e6eb 100644 --- a/validator_derive/src/tokens/cards.rs +++ b/validator_derive/src/tokens/cards.rs @@ -1,22 +1,32 @@ use quote::quote; use crate::types::Card; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn credit_card_tokens( credit_card: Card, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let message = quote_message(credit_card.message); - let code = quote_code(credit_card.code, "credit_card"); + let code = credit_card.code.as_deref().unwrap_or("credit_card"); - quote! { - if !#field_name.validate_credit_card() { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_credit_card() { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::CreditCard { + code: #code.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/contains.rs b/validator_derive/src/tokens/contains.rs index a03dc4dd..60d991cd 100644 --- a/validator_derive/src/tokens/contains.rs +++ b/validator_derive/src/tokens/contains.rs @@ -1,27 +1,38 @@ use quote::quote; use crate::types::Contains; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn contains_tokens( contains: Contains, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let p = contains.pattern; let (needle, needle_err) = (quote!(#p), quote!(err.add_param(::std::borrow::Cow::from("needle"), &#p);)); let message = quote_message(contains.message); - let code = quote_code(contains.code, "contains"); + let code = contains.code.as_deref().unwrap_or("contains"); - quote! { - if !#field_name.validate_contains(#needle) { - #code - #message - #needle_err - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_contains(#needle) { + let mut err = ::validator::ValidationError::new(#code); + #message + #needle_err + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Contains { + code: #code.into(), + pattern: #p.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/custom.rs b/validator_derive/src/tokens/custom.rs index fac74445..3bb182f9 100644 --- a/validator_derive/src/tokens/custom.rs +++ b/validator_derive/src/tokens/custom.rs @@ -1,4 +1,4 @@ -use quote::quote; +use quote::{quote, ToTokens}; use crate::types::Custom; use crate::utils::quote_message; @@ -7,8 +7,9 @@ pub fn custom_tokens( custom: Custom, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let fn_call = custom.function.unwrap(); + let fn_str = fn_call.to_token_stream().to_string(); let args = if let Some(arg) = custom.use_context { if arg { @@ -22,23 +23,37 @@ pub fn custom_tokens( let message = quote_message(custom.message); - let code = if let Some(c) = custom.code { - quote!( - err.code = ::std::borrow::Cow::from(#c); + let (code, code_constraint) = if let Some(c) = custom.code { + ( + quote!( + err.code = ::std::borrow::Cow::from(#c); + ), + quote! { Some(#c.into()) }, ) } else { - quote!() + (quote!(), quote! { None }) }; - quote! { - match #fn_call(#args) { - ::std::result::Result::Ok(()) => {} - ::std::result::Result::Err(mut err) => { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); + ( + quote! { + match #fn_call(#args) { + ::std::result::Result::Ok(()) => {} + ::std::result::Result::Err(mut err) => { + #code + #message + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } } - } - } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Custom { + code: #code_constraint, + function: #fn_str.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/does_not_contain.rs b/validator_derive/src/tokens/does_not_contain.rs index e96e6617..4d5af24a 100644 --- a/validator_derive/src/tokens/does_not_contain.rs +++ b/validator_derive/src/tokens/does_not_contain.rs @@ -1,28 +1,39 @@ use quote::quote; use crate::types::DoesNotContain; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn does_not_contain_tokens( does_not_contain: DoesNotContain, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let p = does_not_contain.pattern; let (needle, needle_err) = (quote!(#p), quote!(err.add_param(::std::borrow::Cow::from("needle"), &#p);)); let message = quote_message(does_not_contain.message); - let code = quote_code(does_not_contain.code, "does_not_contain"); + let code = does_not_contain.code.as_deref().unwrap_or("does_not_contain"); - quote! { - if !#field_name.validate_does_not_contain(#needle) { - #code - #message - #needle_err - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_does_not_contain(#needle) { + let mut err = ::validator::ValidationError::new(#code); + #message + #needle_err + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::DoesNotContain { + code: #code.into(), + pattern: #p.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/email.rs b/validator_derive/src/tokens/email.rs index b44f88c6..ee7e322a 100644 --- a/validator_derive/src/tokens/email.rs +++ b/validator_derive/src/tokens/email.rs @@ -1,22 +1,33 @@ use quote::quote; use crate::types::Email; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn email_tokens( email: Email, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let message = quote_message(email.message); - let code = quote_code(email.code, "email"); + let code = email.code.as_deref().unwrap_or("email"); - quote! { - if !#field_name.validate_email() { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_email() { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Email { + code: #code.into(), + }, + ); + + }, + ) } diff --git a/validator_derive/src/tokens/ip.rs b/validator_derive/src/tokens/ip.rs index 8c002427..6276f0cc 100644 --- a/validator_derive/src/tokens/ip.rs +++ b/validator_derive/src/tokens/ip.rs @@ -1,45 +1,43 @@ use quote::quote; use crate::types::Ip; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn ip_tokens( ip: Ip, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let message = quote_message(ip.message); - let code = quote_code(ip.code, "ip"); + let code = ip.code.as_deref().unwrap_or("ip"); - let version = match (ip.v4, ip.v6) { - (Some(v4), Some(v6)) => match (v4, v6) { - (true, false) => quote!(validate_ipv4()), - (false, true) => quote!(validate_ipv6()), - _ => quote!(validate_ip()), - }, - (Some(v4), None) => { - if v4 { - quote!(validate_ipv4()) - } else { - quote!(validate_ip()) - } - } - (None, Some(v6)) => { - if v6 { - quote!(validate_ipv6()) - } else { - quote!(validate_ip()) - } - } + let v4 = ip.v4.unwrap_or_default(); + let v6 = ip.v6.unwrap_or_default(); + + let version = match (v4, v6) { + (true, false) => quote!(validate_ipv4()), + (false, true) => quote!(validate_ipv6()), _ => quote!(validate_ip()), }; - quote! { - if !#field_name.#version { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.#version { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Ip { + code: #code.into(), + v4: #v4, + v6: #v6, + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/length.rs b/validator_derive/src/tokens/length.rs index 16f78eee..15842ca5 100644 --- a/validator_derive/src/tokens/length.rs +++ b/validator_derive/src/tokens/length.rs @@ -1,13 +1,36 @@ use quote::quote; use crate::types::Length; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn length_tokens( length: Length, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + let constraint = if let Some(eq) = length.equal.as_ref() { + quote! { + ::validator::LengthConstraint::Equal(#eq) + } + } else { + let min = if let Some(ref min) = length.min { + quote! { Some(#min) } + } else { + quote! { None } + }; + let max = if let Some(ref max) = length.max { + quote! { Some(#max) } + } else { + quote! { None } + }; + quote! { + ::validator::LengthConstraint::Range { + min: #min, + max: #max, + } + } + }; + let (min, min_err) = if let Some(v) = length.min.as_ref() { (quote!(Some(#v)), quote!(err.add_param(::std::borrow::Cow::from("min"), &#v);)) } else { @@ -25,17 +48,28 @@ pub fn length_tokens( }; let message = quote_message(length.message); - let code = quote_code(length.code, "length"); + let code = length.code.as_deref().unwrap_or("length"); - quote! { - if !#field_name.validate_length(#min, #max, #equal) { - #code - #message - #min_err - #max_err - #equal_err - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_length(#min, #max, #equal) { + let mut err = ::validator::ValidationError::new(#code); + #message + #min_err + #max_err + #equal_err + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Length { + code: #code.into(), + length: #constraint, + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/must_match.rs b/validator_derive/src/tokens/must_match.rs index 63647f3b..47b20e68 100644 --- a/validator_derive/src/tokens/must_match.rs +++ b/validator_derive/src/tokens/must_match.rs @@ -1,27 +1,37 @@ use quote::quote; use crate::types::MustMatch; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn must_match_tokens( must_match: MustMatch, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { - let o = must_match.other; - let (other, other_err) = - (quote!(self.#o), quote!(err.add_param(::std::borrow::Cow::from("other"), &self.#o);)); +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + let other = must_match.other; + let other_str = other.get_ident().unwrap().to_string(); let message = quote_message(must_match.message); - let code = quote_code(must_match.code, "must_match"); + let code = must_match.code.as_deref().unwrap_or("must_match"); - quote! { - if !::validator::validate_must_match(&#field_name, &#other) { - #code - #message - #other_err - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !::validator::validate_must_match(&#field_name, &self.#other) { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("other"), &self.#other); + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::MustMatch { + code: #code.into(), + other: #other_str.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/nested.rs b/validator_derive/src/tokens/nested.rs index 3af6ffbf..703cb89e 100644 --- a/validator_derive/src/tokens/nested.rs +++ b/validator_derive/src/tokens/nested.rs @@ -1,10 +1,21 @@ use quote::quote; +use syn::Type; pub fn nested_tokens( field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { - quote! { - errors.merge_self(#field_name_str, (&#field_name).validate()); - } + field_type: &Type, +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + ( + quote! { + errors.merge_self(#field_name_str, (&#field_name).validate()); + }, + quote! { + constraints.merge( + #field_name_str, + <#field_type as ::validator::Constraints>::constraints(), + <#field_type as ::validator::Constraints>::is_collection(), + ); + }, + ) } diff --git a/validator_derive/src/tokens/non_control_character.rs b/validator_derive/src/tokens/non_control_character.rs index 8f868ab2..d6e80561 100644 --- a/validator_derive/src/tokens/non_control_character.rs +++ b/validator_derive/src/tokens/non_control_character.rs @@ -1,22 +1,32 @@ use quote::quote; use crate::types::NonControlCharacter; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn non_control_char_tokens( non_control_char: NonControlCharacter, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let message = quote_message(non_control_char.message); - let code = quote_code(non_control_char.code, "non_control_character"); + let code = non_control_char.code.as_deref().unwrap_or("non_control_character"); - quote! { - if !#field_name.validate_non_control_character() { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_non_control_character() { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::NonControlCharacter { + code: #code.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/range.rs b/validator_derive/src/tokens/range.rs index 0c1d8f84..a887865f 100644 --- a/validator_derive/src/tokens/range.rs +++ b/validator_derive/src/tokens/range.rs @@ -1,50 +1,80 @@ use quote::quote; use crate::types::Range; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn range_tokens( range: Range, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { - let (min, min_err) = if let Some(m) = range.min { - (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("min"), &#m);)) +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + let (min, min_err, min_constraint) = if let Some(m) = range.min { + ( + quote!(Some(#m)), + quote!(err.add_param(::std::borrow::Cow::from("min"), &#m);), + quote! { Some(#m.to_string().into()) }, + ) } else { - (quote!(None), quote!()) + (quote!(None), quote!(), quote! { None }) }; - let (max, max_err) = if let Some(m) = range.max { - (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("max"), &#m);)) + let (max, max_err, max_constraint) = if let Some(m) = range.max { + ( + quote!(Some(#m)), + quote!(err.add_param(::std::borrow::Cow::from("max"), &#m);), + quote! { Some(#m.to_string().into()) }, + ) } else { - (quote!(None), quote!()) + (quote!(None), quote!(), quote! { None }) }; - let (ex_min, ex_min_err) = if let Some(m) = range.exclusive_min { - (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("exclusive_min"), &#m);)) + let (ex_min, ex_min_err, ex_min_constraint) = if let Some(m) = range.exclusive_min { + ( + quote!(Some(#m)), + quote!(err.add_param(::std::borrow::Cow::from("exclusive_min"), &#m);), + quote! { Some(#m.to_string().into()) }, + ) } else { - (quote!(None), quote!()) + (quote!(None), quote!(), quote! { None }) }; - let (ex_max, ex_max_err) = if let Some(m) = range.exclusive_max { - (quote!(Some(#m)), quote!(err.add_param(::std::borrow::Cow::from("exclusive_max"), &#m);)) + let (ex_max, ex_max_err, ex_max_constraint) = if let Some(m) = range.exclusive_max { + ( + quote!(Some(#m)), + quote!(err.add_param(::std::borrow::Cow::from("exclusive_max"), &#m);), + quote! { Some(#m.to_string().into()) }, + ) } else { - (quote!(None), quote!()) + (quote!(None), quote!(), quote! { None }) }; let message = quote_message(range.message); - let code = quote_code(range.code, "range"); + let code = range.code.as_deref().unwrap_or("range"); - quote! { - if !#field_name.validate_range(#min, #max, #ex_min, #ex_max) { - #code - #message - #min_err - #max_err - #ex_min_err - #ex_max_err - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_range(#min, #max, #ex_min, #ex_max) { + let mut err = ::validator::ValidationError::new(#code); + #message + #min_err + #max_err + #ex_min_err + #ex_max_err + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Range { + code: #code.into(), + min: #min_constraint, + max: #max_constraint, + exclusive_min: #ex_min_constraint, + exclusive_max: #ex_max_constraint, + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/regex.rs b/validator_derive/src/tokens/regex.rs index cb97331e..07ee62c3 100644 --- a/validator_derive/src/tokens/regex.rs +++ b/validator_derive/src/tokens/regex.rs @@ -1,23 +1,35 @@ -use quote::quote; +use quote::{quote, ToTokens}; use crate::types::Regex; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn regex_tokens( regex: Regex, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let path = regex.path; + let path_str = path.to_token_stream().to_string(); let message = quote_message(regex.message); - let code = quote_code(regex.code, "regex"); + let code = regex.code.as_deref().unwrap_or("regex"); - quote! { - if !&#field_name.validate_regex(&#path) { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !&#field_name.validate_regex(&#path) { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Regex { + code: #code.into(), + path: #path_str.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/required.rs b/validator_derive/src/tokens/required.rs index 22926e02..9e03d17b 100644 --- a/validator_derive/src/tokens/required.rs +++ b/validator_derive/src/tokens/required.rs @@ -2,22 +2,32 @@ use quote::quote; use syn::Ident; use crate::types::Required; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn required_tokens( required: Required, field_name: &Ident, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let message = quote_message(required.message); - let code = quote_code(required.code, "required"); + let code = required.code.as_deref().unwrap_or("required"); - quote! { - if !self.#field_name.validate_required() { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !self.#field_name.validate_required() { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Required { + code: #code.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/tokens/required_nested.rs b/validator_derive/src/tokens/required_nested.rs new file mode 100644 index 00000000..5c69575f --- /dev/null +++ b/validator_derive/src/tokens/required_nested.rs @@ -0,0 +1,37 @@ +use quote::quote; +use syn::Ident; + +use crate::types::Required; +use crate::utils::quote_message; + +pub fn required_nested_tokens( + required: Required, + field_name: &Ident, + field_name_str: &str, +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + let message = quote_message(required.message); + let code = required.code.as_deref().unwrap_or("required"); + + ( + quote! { + if !self.#field_name.validate_required() { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &self.#field_name); + errors.add(#field_name_str, err); + } + + if let Some(ref #field_name) = self.#field_name { + errors.merge_self(#field_name_str, #field_name.validate()); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::RequiredNested { + code: #code.into(), + }, + ); + }, + ) +} diff --git a/validator_derive/src/tokens/url.rs b/validator_derive/src/tokens/url.rs index af38adac..a825cb92 100644 --- a/validator_derive/src/tokens/url.rs +++ b/validator_derive/src/tokens/url.rs @@ -1,22 +1,32 @@ use quote::quote; use crate::types::Url; -use crate::utils::{quote_code, quote_message}; +use crate::utils::quote_message; pub fn url_tokens( url: Url, field_name: &proc_macro2::TokenStream, field_name_str: &str, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let message = quote_message(url.message); - let code = quote_code(url.code, "url"); + let code = url.code.as_deref().unwrap_or("url"); - quote! { - if !#field_name.validate_url() { - #code - #message - err.add_param(::std::borrow::Cow::from("value"), &#field_name); - errors.add(#field_name_str, err); - } - } + ( + quote! { + if !#field_name.validate_url() { + let mut err = ::validator::ValidationError::new(#code); + #message + err.add_param(::std::borrow::Cow::from("value"), &#field_name); + errors.add(#field_name_str, err); + } + }, + quote! { + constraints.add( + #field_name_str, + ::validator::ValidationConstraint::Url { + code: #code.into(), + }, + ); + }, + ) } diff --git a/validator_derive/src/types.rs b/validator_derive/src/types.rs index fe01b5e5..1382e459 100644 --- a/validator_derive/src/types.rs +++ b/validator_derive/src/types.rs @@ -4,10 +4,24 @@ use darling::util::Override; use darling::{FromField, FromMeta}; use proc_macro_error::abort; -use quote::quote; +use quote::{quote, ToTokens}; use syn::spanned::Spanned; use syn::{Expr, Field, Ident, Path}; +use crate::tokens::cards::credit_card_tokens; +use crate::tokens::contains::contains_tokens; +use crate::tokens::custom::custom_tokens; +use crate::tokens::does_not_contain::does_not_contain_tokens; +use crate::tokens::email::email_tokens; +use crate::tokens::ip::ip_tokens; +use crate::tokens::length::length_tokens; +use crate::tokens::must_match::must_match_tokens; +use crate::tokens::nested::nested_tokens; +use crate::tokens::non_control_character::non_control_char_tokens; +use crate::tokens::range::range_tokens; +use crate::tokens::regex::regex_tokens; +use crate::tokens::required::required_tokens; +use crate::tokens::url::url_tokens; use crate::utils::get_attr; static OPTIONS_TYPE: [&str; 3] = ["Option|", "std|option|Option|", "core|option|Option|"]; @@ -216,6 +230,155 @@ impl ValidateField { ), } } + + pub(crate) fn into_tokens(self) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + let field_name = self.ident.clone().unwrap(); + let field_name_str = self.ident.clone().unwrap().to_string(); + + let type_name = self.ty.to_token_stream().to_string(); + let is_number = NUMBER_TYPES.contains(&type_name); + + let (actual_field, wrapper_closure) = self.if_let_option_wrapper(&field_name, is_number); + + let mut validations = Vec::new(); + let mut constraints = Vec::new(); + + macro_rules! constraint { + ($field:expr, $fn:expr) => { + if let Some(value) = $field { + let (validation, constraint) = $fn(value); + validations.push(wrapper_closure(validation)); + constraints.push(constraint); + } + }; + } + + // Length validation + constraint!(self.length, |length| length_tokens(length, &actual_field, &field_name_str)); + + // Email validation + constraint!(self.email, |email| email_tokens( + match email { + Override::Inherit => Email::default(), + Override::Explicit(e) => e, + }, + &actual_field, + &field_name_str, + )); + + // Credit card validation + constraint!(self.credit_card, |credit_card| credit_card_tokens( + match credit_card { + Override::Inherit => Card::default(), + Override::Explicit(c) => c, + }, + &actual_field, + &field_name_str, + )); + + // Url validation + constraint!(self.url, |url| url_tokens( + match url { + Override::Inherit => Url::default(), + Override::Explicit(u) => u, + }, + &actual_field, + &field_name_str, + )); + + // Ip address validation + constraint!(self.ip, |ip| ip_tokens( + match ip { + Override::Inherit => Ip::default(), + Override::Explicit(i) => i, + }, + &actual_field, + &field_name_str, + )); + + // Non control character validation + constraint!(self.non_control_character, |ncc| non_control_char_tokens( + match ncc { + Override::Inherit => NonControlCharacter::default(), + Override::Explicit(n) => n, + }, + &actual_field, + &field_name_str, + )); + + // Range validation + constraint!(self.range, |range| range_tokens(range, &actual_field, &field_name_str)); + + // Required validation + if let Some(required) = self.required { + let (validation, constraint) = required_tokens( + match required { + Override::Inherit => Required::default(), + Override::Explicit(r) => r, + }, + &field_name, + &field_name_str, + ); + validations.push(validation); + constraints.push(constraint); + } + + // Contains validation + constraint!(self.contains, |contains| contains_tokens( + contains, + &actual_field, + &field_name_str + )); + + // Does not contain validation + constraint!(self.does_not_contain, |does_not_contain| does_not_contain_tokens( + does_not_contain, + &actual_field, + &field_name_str + )); + + // Must match validation + // TODO: handle option for other + constraint!(self.must_match, |must_match| must_match_tokens( + must_match, + &actual_field, + &field_name_str + )); + + // Regex validation + constraint!(self.regex, |regex| regex_tokens(regex, &actual_field, &field_name_str)); + + // Custom validation + // We try to be smart when passing arguments + let is_cow = type_name.contains("Cow <"); + let custom_actual_field = if is_cow { + quote!(#actual_field.as_ref()) + } else if is_number || type_name.starts_with("&") { + quote!(#actual_field) + } else { + quote!(&#actual_field) + }; + for c in self.custom { + let (validation, constraint) = custom_tokens(c, &custom_actual_field, &field_name_str); + validations.push(wrapper_closure(validation)); + constraints.push(constraint); + } + + if let Some(true) = self.nested { + let (validation, constraint) = nested_tokens(&actual_field, &field_name_str, &self.ty); + validations.push(wrapper_closure(validation)); + constraints.push(constraint); + } + + ( + quote! { + #(#validations)* + }, + quote! { + #(#constraints)* + }, + ) + } } // Structs to hold the validation information and to provide attributes diff --git a/validator_derive/src/utils.rs b/validator_derive/src/utils.rs index 47b15932..b955a1e9 100644 --- a/validator_derive/src/utils.rs +++ b/validator_derive/src/utils.rs @@ -13,18 +13,6 @@ pub fn quote_message(message: Option) -> proc_macro2::TokenStream { } } -pub fn quote_code(code: Option, default: &str) -> proc_macro2::TokenStream { - if let Some(c) = code { - quote!( - let mut err = ::validator::ValidationError::new(#c); - ) - } else { - quote!( - let mut err = ::validator::ValidationError::new(#default); - ) - } -} - pub fn quote_use_stmts(fields: &Vec) -> proc_macro2::TokenStream { let mut length = quote!(); let mut email = quote!(); diff --git a/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr b/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr index 38903c6c..8f19cabf 100644 --- a/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr +++ b/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr @@ -19,3 +19,20 @@ note: the trait `Validate` must be implemented = note: the following trait defines an item `validate`, perhaps you need to implement it: candidate #1: `Validate` = note: this error originates in the derive macro `Validate` (in Nightly builds, run with -Z macro-backtrace for more info) + +error[E0277]: the trait bound `Nested: Constraints` is not satisfied + --> tests/compile-fail/no_nested_validations.rs:6:13 + | +6 | nested: Nested, + | ^^^^^^ the trait `Constraints` is not implemented for `Nested` + | + = help: the following other types implement trait `Constraints`: + Test + HashMap + indexmap::map::IndexMap + BTreeMap + HashSet + indexmap::set::IndexSet + BTreeSet + Vec + and $N others diff --git a/validator_derive_tests/tests/constraints.rs b/validator_derive_tests/tests/constraints.rs new file mode 100644 index 00000000..d89bb131 --- /dev/null +++ b/validator_derive_tests/tests/constraints.rs @@ -0,0 +1,83 @@ +use validator::{ + Constraints, LengthConstraint, Validate, ValidationConstraint, ValidationConstraintsKind, +}; + +#[derive(Debug, Validate)] +struct A { + #[validate(length(max = 10, code = "a_length"))] + value: String, + + #[validate(nested)] + b: B, +} + +#[derive(Debug, Validate)] +struct B { + #[validate(length(min = 1, code = "b_length"))] + value: String, +} + +#[test] +fn a_constraints_correct() { + let a_constraints = ::constraints(); + + let mut keys = a_constraints.0.keys().collect::>(); + keys.sort(); + assert_eq!(keys, &[&"b", &"value"]); + + { + let value_constraints = &a_constraints.0["value"]; + assert_eq!(value_constraints.len(), 1); + + let struct_constraints = match &value_constraints[0] { + ValidationConstraintsKind::Field(constraints) => constraints, + _ => panic!("Expected a constraint kind of Field, found {:?}", &value_constraints[0]), + }; + + assert_eq!(struct_constraints.len(), 1); + + assert_eq!( + struct_constraints[0], + ValidationConstraint::Length { + length: LengthConstraint::Range { min: None, max: Some(10) }, + code: "a_length".into(), + } + ); + } + + { + let b_constraints = &a_constraints.0["b"]; + assert_eq!(b_constraints.len(), 1); + + let struct_constraints = match &b_constraints[0] { + ValidationConstraintsKind::Struct(constraints) => constraints.as_ref(), + _ => panic!("Expected a constraint kind of Field, found {:?}", &b_constraints[0]), + }; + + assert_eq!(struct_constraints, &::constraints()); + } +} + +#[test] +fn b_constraints_correct() { + let b_constraints = ::constraints(); + assert_eq!(b_constraints.0.keys().collect::>(), &[&"value"]); + + let constraint_kinds = &b_constraints.0["value"]; + assert_eq!(constraint_kinds.len(), 1); + + let struct_constraints = match &constraint_kinds[0] { + ValidationConstraintsKind::Field(constraints) => constraints, + _ => panic!("Expected a constraint kind of Field, found {:?}", &constraint_kinds[0]), + }; + + assert_eq!(struct_constraints.len(), 1); + + assert_eq!( + struct_constraints[0], + ValidationConstraint::Length { + length: LengthConstraint::Range { min: Some(1), max: None }, + code: "b_length".into(), + } + ); +}