diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index 839ee8a3..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,197 +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!( - #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)] @@ -378,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 @@ -402,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