From 4830561e0d18ad63c465869b829847e26c317a7c Mon Sep 17 00:00:00 2001 From: pintariching <64165058+pintariching@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:50:00 +0200 Subject: [PATCH 1/8] Trait validation (#225) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * implemented validation trait for length * converted identation to spaces * changed the trait to not require HasLen * added macro for generating impls * implemented ValidateLength for some types * using trait validation instead of the function * added cfg for indexmap import * changed trait to require length * Revert "changed trait to require length" This reverts commit a77bdc9297a65f9eb3dfa345c92e7cb1aee2525f. * moved validation logic inside ValidateLength trait * added trait validation for required * added email trait validation * fixed trait validation for email * added range trait validation * fixed range trait * added url trait validation --------- Co-authored-by: Tilen Pintarič --- validator/src/lib.rs | 10 +- validator/src/validation/email.rs | 97 +++++++---- validator/src/validation/length.rs | 202 ++++++++++++++++++++--- validator/src/validation/range.rs | 47 ++++-- validator/src/validation/required.rs | 16 +- validator/src/validation/urls.rs | 39 ++++- validator_derive/src/lib.rs | 2 - validator_derive_tests/tests/email.rs | 35 ++++ validator_derive_tests/tests/length.rs | 68 ++++++++ validator_derive_tests/tests/required.rs | 30 ++++ validator_derive_tests/tests/url.rs | 50 ++++++ 11 files changed, 520 insertions(+), 76 deletions(-) diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 8a7a0c63..4bbf3033 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -73,18 +73,18 @@ mod validation; pub use validation::cards::validate_credit_card; pub use validation::contains::validate_contains; pub use validation::does_not_contain::validate_does_not_contain; -pub use validation::email::validate_email; +pub use validation::email::{validate_email, ValidateEmail}; pub use validation::ip::{validate_ip, validate_ip_v4, validate_ip_v6}; -pub use validation::length::validate_length; +pub use validation::length::{validate_length, ValidateLength}; pub use validation::must_match::validate_must_match; #[cfg(feature = "unic")] pub use validation::non_control_character::validate_non_control_character; #[cfg(feature = "phone")] pub use validation::phone::validate_phone; -pub use validation::range::validate_range; +pub use validation::range::{validate_range, ValidateRange}; -pub use validation::required::validate_required; -pub use validation::urls::validate_url; +pub use validation::required::{validate_required, ValidateRequired}; +pub use validation::urls::{validate_url, ValidateUrl}; pub use traits::{Contains, HasLen, Validate, ValidateArgs}; pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind}; diff --git a/validator/src/validation/email.rs b/validator/src/validation/email.rs index e5aab3b6..38b55f8a 100644 --- a/validator/src/validation/email.rs +++ b/validator/src/validation/email.rs @@ -21,39 +21,8 @@ lazy_static! { /// [RFC 5322](https://tools.ietf.org/html/rfc5322) is not practical in most circumstances and allows email addresses /// that are unfamiliar to most users. #[must_use] -pub fn validate_email<'a, T>(val: T) -> bool -where - T: Into>, -{ - let val = val.into(); - if val.is_empty() || !val.contains('@') { - return false; - } - let parts: Vec<&str> = val.rsplitn(2, '@').collect(); - let user_part = parts[1]; - let domain_part = parts[0]; - - // validate the length of each part of the email, BEFORE doing the regex - // according to RFC5321 the max length of the local part is 64 characters - // and the max length of the domain part is 255 characters - // https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1 - if user_part.length() > 64 || domain_part.length() > 255 { - return false; - } - - if !EMAIL_USER_RE.is_match(user_part) { - return false; - } - - if !validate_domain_part(domain_part) { - // Still the possibility of an [IDN](https://en.wikipedia.org/wiki/Internationalized_domain_name) - return match domain_to_ascii(domain_part) { - Ok(d) => validate_domain_part(&d), - Err(_) => false, - }; - } - - true +pub fn validate_email(val: T) -> bool { + val.validate_email() } /// Checks if the domain is a valid domain and if not, check whether it's an IP @@ -73,6 +42,68 @@ fn validate_domain_part(domain_part: &str) -> bool { } } +pub trait ValidateEmail { + fn validate_email(&self) -> bool { + let val = self.to_email_string(); + + if val.is_empty() || !val.contains('@') { + return false; + } + + let parts: Vec<&str> = val.rsplitn(2, '@').collect(); + let user_part = parts[1]; + let domain_part = parts[0]; + + // validate the length of each part of the email, BEFORE doing the regex + // according to RFC5321 the max length of the local part is 64 characters + // and the max length of the domain part is 255 characters + // https://datatracker.ietf.org/doc/html/rfc5321#section-4.5.3.1.1 + if user_part.length() > 64 || domain_part.length() > 255 { + return false; + } + + if !EMAIL_USER_RE.is_match(user_part) { + return false; + } + + if !validate_domain_part(domain_part) { + // Still the possibility of an [IDN](https://en.wikipedia.org/wiki/Internationalized_domain_name) + return match domain_to_ascii(domain_part) { + Ok(d) => validate_domain_part(&d), + Err(_) => false, + }; + } + + true + } + + fn to_email_string<'a>(&'a self) -> Cow<'a, str>; +} + +impl ValidateEmail for &str { + fn to_email_string(&self) -> Cow<'_, str> { + Cow::from(*self) + } +} + +impl ValidateEmail for String { + fn to_email_string(&self) -> Cow<'_, str> { + Cow::from(self) + } +} + +impl ValidateEmail for &String { + fn to_email_string(&self) -> Cow<'_, str> { + Cow::from(*self) + } +} + +impl ValidateEmail for Cow<'_, str> { + fn to_email_string(&self) -> Cow<'_, str> { + self.clone() + } +} + #[cfg(test)] mod tests { use std::borrow::Cow; diff --git a/validator/src/validation/length.rs b/validator/src/validation/length.rs index 759c6810..3f2b0075 100644 --- a/validator/src/validation/length.rs +++ b/validator/src/validation/length.rs @@ -1,4 +1,7 @@ -use crate::traits::HasLen; +use std::{borrow::Cow, collections::{HashMap, HashSet, BTreeMap, BTreeSet}}; + +#[cfg(feature = "indexmap")] +use indexmap::{IndexMap, IndexSet}; /// Validates the length of the value given. /// If the validator has `equal` set, it will ignore any `min` and `max` value. @@ -6,37 +9,156 @@ use crate::traits::HasLen; /// If you apply it on String, don't forget that the length can be different /// from the number of visual characters for Unicode #[must_use] -pub fn validate_length( +pub fn validate_length( value: T, min: Option, max: Option, equal: Option, ) -> bool { - let val_length = value.length(); - - if let Some(eq) = equal { - return val_length == eq; - } else { - if let Some(m) = min { - if val_length < m { - return false; - } - } - if let Some(m) = max { - if val_length > m { - return false; - } - } - } + value.validate_length(min, max, equal) +} + +pub trait ValidateLength { + fn validate_length(&self, min: Option, max: Option, equal: Option) -> bool { + let length = self.length(); + + if let Some(eq) = equal { + return length == eq; + } else { + if let Some(m) = min { + if length < m { + return false; + } + } + if let Some(m) = max { + if length > m { + return false; + } + } + } + + true + } + + fn length(&self) -> u64; +} + +impl ValidateLength for String { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> ValidateLength for &'a String { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> ValidateLength for &'a str { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> ValidateLength for Cow<'a, str> { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl ValidateLength for Vec { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, T> ValidateLength for &'a Vec { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl ValidateLength for &[T] { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl ValidateLength for [T; N] { + fn length(&self) -> u64 { + N as u64 + } +} + +impl ValidateLength for &[T; N] { + fn length(&self) -> u64 { + N as u64 + } +} + +impl<'a, K, V, S> ValidateLength for &'a HashMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl ValidateLength for HashMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, T, S> ValidateLength for &'a HashSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, K, V> ValidateLength for &'a BTreeMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} - true +impl<'a, T> ValidateLength for &'a BTreeSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl ValidateLength for BTreeSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +#[cfg(feature = "indexmap")] +impl<'a, K, V> ValidateLength for &'a IndexMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +#[cfg(feature = "indexmap")] +impl<'a, T> ValidateLength for &'a IndexSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +#[cfg(feature = "indexmap")] +impl ValidateLength for IndexSet { + fn length(&self) -> u64 { + self.len() as u64 + } } #[cfg(test)] mod tests { use std::borrow::Cow; - use super::validate_length; + use crate::{validate_length, validation::length::ValidateLength}; #[test] fn test_validate_length_equal_overrides_min_max() { @@ -76,4 +198,44 @@ mod tests { fn test_validate_length_unicode_chars() { assert!(validate_length("日本", None, None, Some(2))); } + + + #[test] + fn test_validate_length_trait_equal_overrides_min_max() { + assert!(String::from("hello").validate_length(Some(1), Some(2), Some(5))); + } + + #[test] + fn test_validate_length_trait_string_min_max() { + assert!(String::from("hello").validate_length(Some(1), Some(10), None)); + } + + #[test] + fn test_validate_length_trait_string_min_only() { + assert!(!String::from("hello").validate_length(Some(10), None, None)); + } + + #[test] + fn test_validate_length_trait_string_max_only() { + assert!(!String::from("hello").validate_length(None, Some(1), None)); + } + + #[test] + fn test_validate_length_trait_cow() { + let test: Cow<'static, str> = "hello".into(); + assert!(test.validate_length(None, None, Some(5))); + + let test: Cow<'static, str> = String::from("hello").into(); + assert!(test.validate_length(None, None, Some(5))); + } + + #[test] + fn test_validate_length_trait_vec() { + assert!(vec![1, 2, 3].validate_length(None, None, Some(3))); + } + + #[test] + fn test_validate_length_trait_unicode_chars() { + assert!(String::from("日本").validate_length(None, None, Some(2))); + } } diff --git a/validator/src/validation/range.rs b/validator/src/validation/range.rs index 3b3cf6ae..8f339caa 100644 --- a/validator/src/validation/range.rs +++ b/validator/src/validation/range.rs @@ -2,23 +2,50 @@ /// optional and will only be validated if they are not `None` /// #[must_use] -pub fn validate_range(value: T, min: Option, max: Option) -> bool +pub fn validate_range>(value: T, min: Option, max: Option) -> bool { + value.validate_range(min, max) +} + +pub trait ValidateRange { + fn validate_range(&self, min: Option, max: Option) -> bool { + if let Some(max) = max { + if self.greater_than(max) { + return false; + } + } + + if let Some(min) = min { + if self.less_than(min) { + return false; + } + } + + true + } + + fn greater_than(&self, max: T) -> bool; + fn less_than(&self, min: T) -> bool; +} + +impl ValidateRange for T where - T: PartialOrd + PartialEq, + T: PartialEq + PartialOrd, { - if let Some(max) = max { - if value > max { - return false; + fn greater_than(&self, max: T) -> bool { + if self > &max { + return true; } + + false } - if let Some(min) = min { - if value < min { - return false; + fn less_than(&self, min: T) -> bool { + if self < &min { + return true; } - } - true + false + } } #[cfg(test)] diff --git a/validator/src/validation/required.rs b/validator/src/validation/required.rs index 80b06b04..863aeee3 100644 --- a/validator/src/validation/required.rs +++ b/validator/src/validation/required.rs @@ -1,5 +1,19 @@ /// Validates whether the given Option is Some #[must_use] -pub fn validate_required(val: &Option) -> bool { +pub fn validate_required(val: &T) -> bool { val.is_some() } + +pub trait ValidateRequired { + fn validate_required(&self) -> bool { + self.is_some() + } + + fn is_some(&self) -> bool; +} + +impl ValidateRequired for Option { + fn is_some(&self) -> bool { + self.is_some() + } +} diff --git a/validator/src/validation/urls.rs b/validator/src/validation/urls.rs index 41774d1a..1528dcd4 100644 --- a/validator/src/validation/urls.rs +++ b/validator/src/validation/urls.rs @@ -3,11 +3,40 @@ use url::Url; /// Validates whether the string given is a url #[must_use] -pub fn validate_url<'a, T>(val: T) -> bool -where - T: Into>, -{ - Url::parse(val.into().as_ref()).is_ok() +pub fn validate_url(val: T) -> bool { + val.validate_url() +} + +pub trait ValidateUrl { + fn validate_url(&self) -> bool { + Url::parse(&self.to_url_string()).is_ok() + } + + fn to_url_string<'a>(&'a self) -> Cow<'a, str>; +} + +impl ValidateUrl for &str { + fn to_url_string(&self) -> Cow<'_, str> { + Cow::from(*self) + } +} + +impl ValidateUrl for String { + fn to_url_string(&self) -> Cow<'_, str> { + Cow::from(self) + } +} + +impl ValidateUrl for &String { + fn to_url_string(&self) -> Cow<'_, str> { + Cow::from(*self) + } +} + +impl ValidateUrl for Cow<'_, str> { + fn to_url_string(&self) -> Cow<'_, str> { + self.clone() + } } #[cfg(test)] diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index fdecfbd1..143fbd00 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -408,11 +408,9 @@ fn find_validators_for_field( syn::Meta::Path(ref name) => { match name.get_ident().unwrap().to_string().as_ref() { "email" => { - assert_string_type("email", field_type, &field.ty); validators.push(FieldValidation::new(Validator::Email)); } "url" => { - assert_string_type("url", field_type, &field.ty); validators.push(FieldValidation::new(Validator::Url)); } #[cfg(feature = "phone")] diff --git a/validator_derive_tests/tests/email.rs b/validator_derive_tests/tests/email.rs index 694b357f..0eaf4ca6 100644 --- a/validator_derive_tests/tests/email.rs +++ b/validator_derive_tests/tests/email.rs @@ -1,3 +1,4 @@ +use serde::Serialize; use validator::Validate; #[test] @@ -65,3 +66,37 @@ fn can_specify_message_for_email() { assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); } + +#[test] +fn can_validate_custom_impl_for_email() { + use std::borrow::Cow; + + #[derive(Debug, Serialize)] + struct CustomEmail { + user_part: String, + domain_part: String, + } + + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(email)] + val: CustomEmail, + } + + impl validator::ValidateEmail for &CustomEmail { + fn to_email_string(&self) -> Cow<'_, str> { + Cow::from(format!("{}@{}", self.user_part, self.domain_part)) + } + } + + let valid = TestStruct { + val: CustomEmail { user_part: "username".to_string(), domain_part: "gmail.com".to_owned() }, + }; + + let invalid = TestStruct { + val: CustomEmail { user_part: "abc".to_string(), domain_part: "".to_owned() }, + }; + + assert!(valid.validate().is_ok()); + assert!(invalid.validate().is_err()); +} diff --git a/validator_derive_tests/tests/length.rs b/validator_derive_tests/tests/length.rs index 317b2999..aa5de1fe 100644 --- a/validator_derive_tests/tests/length.rs +++ b/validator_derive_tests/tests/length.rs @@ -222,3 +222,71 @@ fn can_validate_set_ref_for_length() { assert_eq!(errs["val"][0].params["min"], 5); assert_eq!(errs["val"][0].params["max"], 10); } + +#[test] +fn can_validate_custom_impl_for_length() { + use serde::Serialize; + + #[derive(Debug, Serialize)] + struct CustomString(String); + + impl validator::ValidateLength for &CustomString { + fn validate_length(&self, min: Option, max: Option, equal: Option) -> bool { + let length = self.length(); + + if let Some(eq) = equal { + return length == eq; + } else { + if let Some(m) = min { + if length < m { + return false; + } + } + if let Some(m) = max { + if length > m { + return false; + } + } + } + + true + } + + fn length(&self) -> u64 { + self.0.chars().count() as u64 + } + } + + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(length(min = 5, max = 10))] + val: CustomString, + } + + #[derive(Debug, Validate)] + struct EqualsTestStruct { + #[validate(length(equal = 11))] + val: CustomString + } + + let too_short = TestStruct { + val: CustomString(String::from("oops")) + }; + + let too_long = TestStruct { + val: CustomString(String::from("too long for this")) + }; + + let ok = TestStruct { + val: CustomString(String::from("perfect")) + }; + + let equals_ok = EqualsTestStruct { + val: CustomString(String::from("just enough")) + }; + + assert!(too_short.validate().is_err()); + assert!(too_long.validate().is_err()); + assert!(ok.validate().is_ok()); + assert!(equals_ok.validate().is_ok()); +} \ No newline at end of file diff --git a/validator_derive_tests/tests/required.rs b/validator_derive_tests/tests/required.rs index f03d6653..c9f751b1 100644 --- a/validator_derive_tests/tests/required.rs +++ b/validator_derive_tests/tests/required.rs @@ -90,3 +90,33 @@ fn can_specify_message_for_required() { assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); } + +#[test] +fn can_validate_custom_impl_for_required() { + #[derive(Debug, Serialize)] + enum CustomOption { + Something(T), + Nothing, + } + + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(required)] + val: CustomOption, + } + + impl validator::ValidateRequired for CustomOption { + fn is_some(&self) -> bool { + match self { + CustomOption::Something(_) => true, + CustomOption::Nothing => false, + } + } + } + + let something = TestStruct { val: CustomOption::Something("this is something".to_string()) }; + let nothing = TestStruct { val: CustomOption::Nothing }; + + assert!(something.validate().is_ok()); + assert!(nothing.validate().is_err()); +} \ No newline at end of file diff --git a/validator_derive_tests/tests/url.rs b/validator_derive_tests/tests/url.rs index 88d2f48b..fe8d4888 100644 --- a/validator_derive_tests/tests/url.rs +++ b/validator_derive_tests/tests/url.rs @@ -67,3 +67,53 @@ fn can_specify_message_for_url() { assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); } + +#[test] +fn can_validate_custom_impl_for_url() { + use serde::Serialize; + use std::borrow::Cow; + + #[derive(Debug, Serialize)] + struct CustomUrl { + scheme: String, + subdomain: String, + domain: String, + top_level_domain: String, + } + + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(url)] + val: CustomUrl, + } + + impl validator::ValidateUrl for &CustomUrl { + fn to_url_string(&self) -> Cow<'_, str> { + Cow::from(format!( + "{}://{}.{}.{}", + self.scheme, self.subdomain, self.domain, self.top_level_domain + )) + } + } + + let valid = TestStruct { + val: CustomUrl { + scheme: "http".to_string(), + subdomain: "www".to_string(), + domain: "google".to_string(), + top_level_domain: "com".to_string(), + }, + }; + + let invalid = TestStruct { + val: CustomUrl { + scheme: "".to_string(), + subdomain: "".to_string(), + domain: "google".to_string(), + top_level_domain: "".to_string(), + }, + }; + + assert!(valid.validate().is_ok()); + assert!(invalid.validate().is_err()); +} From 6471baee36963fe3a8f4134ecdb0003fc59e434f Mon Sep 17 00:00:00 2001 From: Ivan GJ <122645806+ivan-gj@users.noreply.github.com> Date: Sun, 23 Apr 2023 23:46:00 +0200 Subject: [PATCH 2/8] Feature: Add `exclusive_min` and `exclusive_max` to `range` validation (#246) * feat(range): add exclusive minimum and exclusive maximum for `range` validation * test(range): add tests for `exc_min` and `exc_max` range validation * docs: add docs for `exc_min` and `exc_max` for `range` validation * chore: rename `exc_min`, `exc_max` to `exclusive_min`, `exclusive_max` * chore(validation.rs): get rid of `collide` function --- README.md | 7 +- validator/src/validation/range.rs | 78 +++++++++++++++---- validator_derive/src/quoting.rs | 59 ++++++++------ validator_derive/src/validation.rs | 52 +++++++++++-- .../tests/compile-fail/range/no_args.stderr | 2 +- validator_derive_tests/tests/complex.rs | 12 +-- validator_derive_tests/tests/range.rs | 78 +++++++++++++++++++ .../tests/run-pass/range.rs | 25 ++++++ validator_types/src/lib.rs | 2 + 9 files changed, 263 insertions(+), 52 deletions(-) diff --git a/README.md b/README.md index e915726b..cb0f5f50 100644 --- a/README.md +++ b/README.md @@ -54,6 +54,8 @@ struct SignupData { first_name: String, #[validate(range(min = 18, max = 20))] age: u32, + #[validate(range(exclusive_min = 0.0, max = 100.0))] + height: f32, } fn validate_unique_username(username: &str) -> Result<(), ValidationError> { @@ -197,7 +199,8 @@ const MAX_CONST: u64 = 10; ``` ### range -Tests whether a number is in the given range. `range` takes 1 or 2 arguments `min` and `max` that can be a number or a value path. +Tests whether a number is in the given range. `range` takes 1 or 2 arguments, and they can be normal (`min` and `max`) or exclusive (`exclusive_min`, `exclusive_max`, unreachable limits). +These can be a number or a value path. Examples: @@ -212,6 +215,8 @@ const MIN_CONSTANT: i32 = 0; #[validate(range(max = 10.8))] #[validate(range(min = "MAX_CONSTANT"))] #[validate(range(min = "crate::MAX_CONSTANT"))] +#[validate(range(exclusive_min = 0.0, max = 100.0))] +#[validate(range(exclusive_max = 10))] ``` ### must_match diff --git a/validator/src/validation/range.rs b/validator/src/validation/range.rs index 8f339caa..20fa5e80 100644 --- a/validator/src/validation/range.rs +++ b/validator/src/validation/range.rs @@ -1,13 +1,26 @@ -/// Validates that the given `value` is inside the defined range. The `max` and `min` parameters are +/// Validates that the given `value` is inside the defined range. +/// The `max`, `min`, `exclusive_max` and `exclusive_min` parameters are /// optional and will only be validated if they are not `None` /// #[must_use] -pub fn validate_range>(value: T, min: Option, max: Option) -> bool { - value.validate_range(min, max) +pub fn validate_range>( + value: T, + min: Option, + max: Option, + exclusive_min: Option, + exclusive_max: Option, +) -> bool { + value.validate_range(min, max, exclusive_min, exclusive_max) } pub trait ValidateRange { - fn validate_range(&self, min: Option, max: Option) -> bool { + fn validate_range( + &self, + min: Option, + max: Option, + exclusive_min: Option, + exclusive_max: Option, + ) -> bool { if let Some(max) = max { if self.greater_than(max) { return false; @@ -20,6 +33,18 @@ pub trait ValidateRange { } } + if let Some(exclusive_max) = exclusive_max { + if !self.less_than(exclusive_max) { + return false; + } + } + + if let Some(exclusive_min) = exclusive_min { + if !self.greater_than(exclusive_min) { + return false; + } + } + true } @@ -55,30 +80,53 @@ mod tests { #[test] fn test_validate_range_generic_ok() { // Unspecified generic type: - assert!(validate_range(10, Some(-10), Some(10))); - assert!(validate_range(0.0, Some(0.0), Some(10.0))); + assert!(validate_range(10, Some(-10), Some(10), None, None)); + assert!(validate_range(0.0, Some(0.0), Some(10.0), None, None)); // Specified type: - assert!(validate_range(5u8, Some(0), Some(255))); - assert!(validate_range(4u16, Some(0), Some(16))); - assert!(validate_range(6u32, Some(0), Some(23))); + assert!(validate_range(5u8, Some(0), Some(255), None, None)); + assert!(validate_range(4u16, Some(0), Some(16), None, None)); + assert!(validate_range(6u32, Some(0), Some(23), None, None)); } #[test] fn test_validate_range_generic_fail() { - assert!(!validate_range(5, Some(17), Some(19))); - assert!(!validate_range(-1.0, Some(0.0), Some(10.0))); + assert!(!validate_range(5, Some(17), Some(19), None, None)); + assert!(!validate_range(-1.0, Some(0.0), Some(10.0), None, None)); } #[test] fn test_validate_range_generic_min_only() { - assert!(!validate_range(5, Some(10), None)); - assert!(validate_range(15, Some(10), None)); + assert!(!validate_range(5, Some(10), None, None, None)); + assert!(validate_range(15, Some(10), None, None, None)); } #[test] fn test_validate_range_generic_max_only() { - assert!(validate_range(5, None, Some(10))); - assert!(!validate_range(15, None, Some(10))); + assert!(validate_range(5, None, Some(10), None, None)); + assert!(!validate_range(15, None, Some(10), None, None)); + } + + #[test] + fn test_validate_range_generic_exc_ok() { + assert!(validate_range(6, None, None, Some(5), Some(7))); + assert!(validate_range(0.0001, None, None, Some(0.0), Some(1.0))); + } + + #[test] + fn test_validate_range_generic_exc_fail() { + assert!(!validate_range(5, None, None, Some(5), None)); + } + + #[test] + fn test_validate_range_generic_exclusive_max_only() { + assert!(!validate_range(10, None, None, None, Some(10))); + assert!(validate_range(9, None, None, None, Some(10))); + } + + #[test] + fn test_validate_range_generic_exclusive_min_only() { + assert!(!validate_range(10, None, None, Some(10), None)); + assert!(validate_range(9, None, None, Some(8), None)); } } diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs index 03e40a31..3d0f5cca 100644 --- a/validator_derive/src/quoting.rs +++ b/validator_derive/src/quoting.rs @@ -2,7 +2,7 @@ use if_chain::if_chain; use proc_macro2::{self, Span}; use quote::quote; -use validator_types::Validator; +use validator_types::{Validator, ValueOrPath}; use crate::asserts::{COW_TYPE, NUMBER_TYPES}; use crate::lit::{option_to_tokens, value_or_path_to_tokens}; @@ -230,39 +230,34 @@ pub fn quote_range_validation( let field_name = &field_quoter.name; let quoted_ident = field_quoter.quote_validator_param(); - if let Validator::Range { ref min, ref max } = validation.validator { - let min_err_param_quoted = if let Some(v) = min { - let v = value_or_path_to_tokens(v); - quote!(err.add_param(::std::borrow::Cow::from("min"), &#v);) - } else { - quote!() - }; - let max_err_param_quoted = if let Some(v) = max { - let v = value_or_path_to_tokens(v); - quote!(err.add_param(::std::borrow::Cow::from("max"), &#v);) - } else { - quote!() - }; + if let Validator::Range { ref min, ref max, ref exclusive_min, ref exclusive_max } = + validation.validator + { + let min_err_param_quoted = err_param_quoted(min, "min"); + let max_err_param_quoted = err_param_quoted(max, "max"); + let exclusive_min_err_param_quoted = err_param_quoted(exclusive_min, "exclusive_min"); + let exclusive_max_err_param_quoted = err_param_quoted(exclusive_max, "exclusive_max"); // Can't interpolate None - let min_tokens = - min.clone().map(|x| value_or_path_to_tokens(&x)).map(|x| quote!(#x as f64)); - let min_tokens = option_to_tokens(&min_tokens); - - let max_tokens = - max.clone().map(|x| value_or_path_to_tokens(&x)).map(|x| quote!(#x as f64)); - let max_tokens = option_to_tokens(&max_tokens); + let min_tokens = generate_tokens(min); + let max_tokens = generate_tokens(max); + let exclusive_min_tokens = generate_tokens(exclusive_min); + let exclusive_max_tokens = generate_tokens(exclusive_max); let quoted_error = quote_error(validation); let quoted = quote!( if !::validator::validate_range( #quoted_ident as f64, #min_tokens, - #max_tokens + #max_tokens, + #exclusive_min_tokens, + #exclusive_max_tokens, ) { #quoted_error #min_err_param_quoted #max_err_param_quoted + #exclusive_min_err_param_quoted + #exclusive_max_err_param_quoted err.add_param(::std::borrow::Cow::from("value"), &#quoted_ident); errors.add(#field_name, err); } @@ -274,6 +269,26 @@ pub fn quote_range_validation( unreachable!() } +fn err_param_quoted(option: &Option>, name: &str) -> proc_macro2::TokenStream +where + T: std::fmt::Debug + std::clone::Clone + std::cmp::PartialEq + quote::ToTokens, +{ + if let Some(v) = option { + let v = value_or_path_to_tokens(v); + quote!(err.add_param(::std::borrow::Cow::from(#name), &#v);) + } else { + quote!() + } +} + +fn generate_tokens(value: &Option>) -> proc_macro2::TokenStream +where + T: std::fmt::Debug + std::clone::Clone + std::cmp::PartialEq + quote::ToTokens, +{ + let tokens = value.clone().map(|x| value_or_path_to_tokens(&x)).map(|x| quote!(#x as f64)); + option_to_tokens(&tokens) +} + #[cfg(feature = "card")] pub fn quote_credit_card_validation( field_quoter: &FieldQuoter, diff --git a/validator_derive/src/validation.rs b/validator_derive/src/validation.rs index b889ccad..a9397707 100644 --- a/validator_derive/src/validation.rs +++ b/validator_derive/src/validation.rs @@ -124,6 +124,11 @@ pub fn extract_length_validation( } } +const RANGE_MIN_KEY: &str = "min"; +const RANGE_EXCLUSIVE_MIN_KEY: &str = "exclusive_min"; +const RANGE_MAX_KEY: &str = "max"; +const RANGE_EXCLUSIVE_MAX_KEY: &str = "exclusive_max"; + pub fn extract_range_validation( field: String, attr: &syn::Attribute, @@ -131,6 +136,8 @@ pub fn extract_range_validation( ) -> FieldValidation { let mut min = None; let mut max = None; + let mut exclusive_min = None; + let mut exclusive_max = None; let (message, code) = extract_message_and_code("range", &field, meta_items); @@ -145,16 +152,28 @@ pub fn extract_range_validation( let ident = path.get_ident().unwrap(); match ident.to_string().as_ref() { "message" | "code" => continue, - "min" => { + RANGE_MIN_KEY => { min = match lit_to_f64_or_path(lit) { Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `min` of `range` validator: only number literals or value paths are allowed") + None => error(lit.span(), &lit_to_f64_error_message(RANGE_MIN_KEY)) + }; + } + RANGE_EXCLUSIVE_MIN_KEY => { + exclusive_min = match lit_to_f64_or_path(lit) { + Some(s) => Some(s), + None => error(lit.span(), &lit_to_f64_error_message(RANGE_EXCLUSIVE_MIN_KEY)) }; } - "max" => { + RANGE_MAX_KEY => { max = match lit_to_f64_or_path(lit) { Some(s) => Some(s), - None => error(lit.span(), "invalid argument type for `max` of `range` validator: only number literals or value paths are allowed") + None => error(lit.span(), &lit_to_f64_error_message(RANGE_MAX_KEY)) + }; + } + RANGE_EXCLUSIVE_MAX_KEY => { + exclusive_max = match lit_to_f64_or_path(lit) { + Some(s) => Some(s), + None => error(lit.span(), &lit_to_f64_error_message(RANGE_EXCLUSIVE_MAX_KEY)) }; } v => error(path.span(), &format!( @@ -173,11 +192,26 @@ pub fn extract_range_validation( } } - if min.is_none() && max.is_none() { - error(attr.span(), "Validator `range` requires at least 1 argument out of `min` and `max`"); + if [&min, &max, &exclusive_min, &exclusive_max].iter().all(|x| x.is_none()) { + error( + attr.span(), + &format!( + "Validator `range` requires at least 1 argument out of `{}`, `{}`, `{}` and `{}`", + RANGE_MIN_KEY, RANGE_MAX_KEY, RANGE_EXCLUSIVE_MIN_KEY, RANGE_EXCLUSIVE_MAX_KEY + ), + ); } - let validator = Validator::Range { min, max }; + if min.is_some() && exclusive_min.is_some() || max.is_some() && exclusive_max.is_some() { + error( + attr.span(), + &format!( + "Validator `range` cannot contain one of its limits (`{}`, `{}`) and its exclusive counterpart", + RANGE_MIN_KEY, RANGE_MAX_KEY + ) + ) + } + let validator = Validator::Range { min, max, exclusive_min, exclusive_max }; FieldValidation { message, code: code.unwrap_or_else(|| validator.code().to_string()), @@ -185,6 +219,10 @@ pub fn extract_range_validation( } } +fn lit_to_f64_error_message(val_name: &str) -> String { + format!("invalid argument type for `{}` of `range` validator: only number literals or value paths are allowed", val_name) +} + pub fn extract_custom_validation( field: String, attr: &syn::Attribute, diff --git a/validator_derive_tests/tests/compile-fail/range/no_args.stderr b/validator_derive_tests/tests/compile-fail/range/no_args.stderr index f3892788..5e6ef1ac 100644 --- a/validator_derive_tests/tests/compile-fail/range/no_args.stderr +++ b/validator_derive_tests/tests/compile-fail/range/no_args.stderr @@ -1,4 +1,4 @@ -error: Invalid attribute #[validate] on field `s`: Validator `range` requires at least 1 argument out of `min` and `max` +error: Invalid attribute #[validate] on field `s`: Validator `range` requires at least 1 argument out of `min`, `max`, `exclusive_min` and `exclusive_max` --> $DIR/no_args.rs:5:5 | 5 | #[validate(range())] diff --git a/validator_derive_tests/tests/complex.rs b/validator_derive_tests/tests/complex.rs index 9a39f6de..5f357349 100644 --- a/validator_derive_tests/tests/complex.rs +++ b/validator_derive_tests/tests/complex.rs @@ -167,9 +167,9 @@ fn test_can_validate_option_fields_with_lifetime() { name: Option<&'a str>, #[validate(length(min = 1, max = 10))] address: Option>, - #[validate(range(min = 1, max = 100))] + #[validate(range(exclusive_min = 0, max = 100))] age: Option>, - #[validate(range(min = 1, max = 10))] + #[validate(range(min = 1, exclusive_max = 10))] range: Option, #[validate(email)] email: Option<&'a str>, @@ -217,9 +217,9 @@ fn test_can_validate_option_fields_without_lifetime() { ids: Option>, #[validate(length(min = 1, max = 10))] opt_ids: Option>>, - #[validate(range(min = 1, max = 100))] + #[validate(range(exclusive_min = 0, max = 100))] age: Option>, - #[validate(range(min = 1, max = 10))] + #[validate(range(min = 1, exclusive_max = 10))] range: Option, #[validate(email)] email: Option, @@ -281,9 +281,9 @@ fn test_works_with_none_values() { name: Option, #[validate(length(min = 1, max = 10))] address: Option>, - #[validate(range(min = 1, max = 100))] + #[validate(range(exclusive_min = 0, max = 100))] age: Option>, - #[validate(range(min = 1, max = 10))] + #[validate(range(min = 1, exclusive_max = 10))] range: Option, } diff --git a/validator_derive_tests/tests/range.rs b/validator_derive_tests/tests/range.rs index 7af240b0..f3ef9fa0 100644 --- a/validator_derive_tests/tests/range.rs +++ b/validator_derive_tests/tests/range.rs @@ -23,6 +23,45 @@ fn can_validate_range_ok() { assert!(s.validate().is_ok()); } +#[test] +fn can_validate_range_exclusive_min_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(exclusive_min = 5, max = 10))] + val: usize, + } + + let s = TestStruct { val: 6 }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn can_validate_range_exclusive_max_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(min = 5, exclusive_max = 10))] + val: usize, + } + + let s = TestStruct { val: 9 }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn can_validate_exclusive_min_and_max_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(exclusive_min = 5, exclusive_max = 10))] + val: usize, + } + + let s = TestStruct { val: 6 }; + + assert!(s.validate().is_ok()); +} + #[test] fn can_validate_only_min_ok() { #[derive(Debug, Validate)] @@ -49,6 +88,32 @@ fn can_validate_only_max_ok() { assert!(s.validate().is_ok()); } +#[test] +fn can_validate_only_exclusive_min_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(exclusive_min = 5))] + val: usize, + } + + let s = TestStruct { val: 6 }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn can_validate_only_exclusive_max_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(exclusive_max = 50))] + val: usize, + } + + let s = TestStruct { val: 49 }; + + assert!(s.validate().is_ok()); +} + #[test] fn can_validate_range_value_crate_path_ok() { #[derive(Debug, Validate)] @@ -62,6 +127,19 @@ fn can_validate_range_value_crate_path_ok() { assert!(s.validate().is_ok()); } +#[test] +fn can_validate_exclusive_range_value_crate_path_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(exclusive_min = "MIN_CONST", max = "MAX_CONST"))] + val: usize, + } + + let s = TestStruct { val: 6 }; + + assert!(s.validate().is_ok()); +} + #[test] fn value_out_of_range_fails_validation() { #[derive(Debug, Validate)] diff --git a/validator_derive_tests/tests/run-pass/range.rs b/validator_derive_tests/tests/run-pass/range.rs index fda45ca0..bff78551 100644 --- a/validator_derive_tests/tests/run-pass/range.rs +++ b/validator_derive_tests/tests/run-pass/range.rs @@ -26,4 +26,29 @@ struct Test { s11: Option, } +#[derive(Validate)] +struct Test2 { + #[validate(range(exclusive_min = 1.1, max = 2.2))] + s: isize, + #[validate(range(min = 1, exclusive_max = 2))] + s2: usize, + #[validate(range(exclusive_min = 18, exclusive_max = 22))] + s3: i32, + #[validate(range(exclusive_min = 18, max = 22))] + s4: i64, + #[validate(range(min = 18, exclusive_max = 22))] + s5: u32, + #[validate(range(min = 18, exclusive_max = 22))] + s6: u64, + #[validate(range(exclusive_min = 18.1, max = 22))] + s7: i8, + #[validate(range(exclusive_min = 18.0, max = 22))] + s8: u8, + #[validate(range(exclusive_min = 18.0, exclusive_max = 22))] + s9: Option, + #[validate(range(exclusive_min = 18.0))] + s10: Option, + #[validate(range(exclusive_max = 18.0))] + s11: Option, +} fn main() {} diff --git a/validator_types/src/lib.rs b/validator_types/src/lib.rs index 42dbdaff..1f96eca0 100644 --- a/validator_types/src/lib.rs +++ b/validator_types/src/lib.rs @@ -24,6 +24,8 @@ pub enum Validator { Range { min: Option>, max: Option>, + exclusive_min: Option>, + exclusive_max: Option>, }, // Any value that impl HasLen can be validated with Length Length { From 61531d3f829865f1d806d7a2a6750626ffdf4b0b Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 12 May 2023 21:44:37 -0400 Subject: [PATCH 3/8] Change some trait + cargo fmt --- validator/src/validation/email.rs | 24 +---- validator/src/validation/length.rs | 124 ++++++++++++----------- validator/src/validation/urls.rs | 24 +---- validator_derive_tests/tests/length.rs | 78 +++++++------- validator_derive_tests/tests/required.rs | 2 +- 5 files changed, 105 insertions(+), 147 deletions(-) diff --git a/validator/src/validation/email.rs b/validator/src/validation/email.rs index 38b55f8a..0b386eba 100644 --- a/validator/src/validation/email.rs +++ b/validator/src/validation/email.rs @@ -77,30 +77,12 @@ pub trait ValidateEmail { true } - fn to_email_string<'a>(&'a self) -> Cow<'a, str>; + fn to_email_string(&self) -> Cow; } -impl ValidateEmail for &str { +impl> ValidateEmail for T { fn to_email_string(&self) -> Cow<'_, str> { - Cow::from(*self) - } -} - -impl ValidateEmail for String { - fn to_email_string(&self) -> Cow<'_, str> { - Cow::from(self) - } -} - -impl ValidateEmail for &String { - fn to_email_string(&self) -> Cow<'_, str> { - Cow::from(*self) - } -} - -impl ValidateEmail for Cow<'_, str> { - fn to_email_string(&self) -> Cow<'_, str> { - self.clone() + Cow::from(self.as_ref()) } } diff --git a/validator/src/validation/length.rs b/validator/src/validation/length.rs index 3f2b0075..2b2b3d8e 100644 --- a/validator/src/validation/length.rs +++ b/validator/src/validation/length.rs @@ -1,4 +1,7 @@ -use std::{borrow::Cow, collections::{HashMap, HashSet, BTreeMap, BTreeSet}}; +use std::{ + borrow::Cow, + collections::{BTreeMap, BTreeSet, HashMap, HashSet}, +}; #[cfg(feature = "indexmap")] use indexmap::{IndexMap, IndexSet}; @@ -20,138 +23,138 @@ pub fn validate_length( pub trait ValidateLength { fn validate_length(&self, min: Option, max: Option, equal: Option) -> bool { - let length = self.length(); - - if let Some(eq) = equal { - return length == eq; - } else { - if let Some(m) = min { - if length < m { - return false; - } - } - if let Some(m) = max { - if length > m { - return false; - } - } - } - - true - } - - fn length(&self) -> u64; + let length = self.length(); + + if let Some(eq) = equal { + return length == eq; + } else { + if let Some(m) = min { + if length < m { + return false; + } + } + if let Some(m) = max { + if length > m { + return false; + } + } + } + + true + } + + fn length(&self) -> u64; } impl ValidateLength for String { - fn length(&self) -> u64 { - self.chars().count() as u64 - } + fn length(&self) -> u64 { + self.chars().count() as u64 + } } impl<'a> ValidateLength for &'a String { fn length(&self) -> u64 { - self.chars().count() as u64 - } + self.chars().count() as u64 + } } impl<'a> ValidateLength for &'a str { fn length(&self) -> u64 { - self.chars().count() as u64 - } + self.chars().count() as u64 + } } impl<'a> ValidateLength for Cow<'a, str> { fn length(&self) -> u64 { - self.chars().count() as u64 - } + self.chars().count() as u64 + } } impl ValidateLength for Vec { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl<'a, T> ValidateLength for &'a Vec { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl ValidateLength for &[T] { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl ValidateLength for [T; N] { - fn length(&self) -> u64 { - N as u64 - } + fn length(&self) -> u64 { + N as u64 + } } impl ValidateLength for &[T; N] { fn length(&self) -> u64 { - N as u64 - } + N as u64 + } } impl<'a, K, V, S> ValidateLength for &'a HashMap { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl ValidateLength for HashMap { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl<'a, T, S> ValidateLength for &'a HashSet { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl<'a, K, V> ValidateLength for &'a BTreeMap { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl<'a, T> ValidateLength for &'a BTreeSet { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } impl ValidateLength for BTreeSet { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } #[cfg(feature = "indexmap")] impl<'a, K, V> ValidateLength for &'a IndexMap { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } #[cfg(feature = "indexmap")] impl<'a, T> ValidateLength for &'a IndexSet { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } #[cfg(feature = "indexmap")] impl ValidateLength for IndexSet { fn length(&self) -> u64 { - self.len() as u64 - } + self.len() as u64 + } } #[cfg(test)] @@ -199,7 +202,6 @@ mod tests { assert!(validate_length("日本", None, None, Some(2))); } - #[test] fn test_validate_length_trait_equal_overrides_min_max() { assert!(String::from("hello").validate_length(Some(1), Some(2), Some(5))); diff --git a/validator/src/validation/urls.rs b/validator/src/validation/urls.rs index 1528dcd4..a2c05e98 100644 --- a/validator/src/validation/urls.rs +++ b/validator/src/validation/urls.rs @@ -12,30 +12,12 @@ pub trait ValidateUrl { Url::parse(&self.to_url_string()).is_ok() } - fn to_url_string<'a>(&'a self) -> Cow<'a, str>; + fn to_url_string(&self) -> Cow; } -impl ValidateUrl for &str { +impl> ValidateUrl for T { fn to_url_string(&self) -> Cow<'_, str> { - Cow::from(*self) - } -} - -impl ValidateUrl for String { - fn to_url_string(&self) -> Cow<'_, str> { - Cow::from(self) - } -} - -impl ValidateUrl for &String { - fn to_url_string(&self) -> Cow<'_, str> { - Cow::from(*self) - } -} - -impl ValidateUrl for Cow<'_, str> { - fn to_url_string(&self) -> Cow<'_, str> { - self.clone() + Cow::from(self.as_ref()) } } diff --git a/validator_derive_tests/tests/length.rs b/validator_derive_tests/tests/length.rs index aa5de1fe..f8529d5c 100644 --- a/validator_derive_tests/tests/length.rs +++ b/validator_derive_tests/tests/length.rs @@ -225,36 +225,36 @@ fn can_validate_set_ref_for_length() { #[test] fn can_validate_custom_impl_for_length() { - use serde::Serialize; + use serde::Serialize; - #[derive(Debug, Serialize)] + #[derive(Debug, Serialize)] struct CustomString(String); impl validator::ValidateLength for &CustomString { fn validate_length(&self, min: Option, max: Option, equal: Option) -> bool { - let length = self.length(); - - if let Some(eq) = equal { - return length == eq; - } else { - if let Some(m) = min { - if length < m { - return false; - } - } - if let Some(m) = max { - if length > m { - return false; - } - } - } - - true + let length = self.length(); + + if let Some(eq) = equal { + return length == eq; + } else { + if let Some(m) = min { + if length < m { + return false; + } + } + if let Some(m) = max { + if length > m { + return false; + } + } + } + + true } - fn length(&self) -> u64 { - self.0.chars().count() as u64 - } + fn length(&self) -> u64 { + self.0.chars().count() as u64 + } } #[derive(Debug, Validate)] @@ -263,30 +263,22 @@ fn can_validate_custom_impl_for_length() { val: CustomString, } - #[derive(Debug, Validate)] - struct EqualsTestStruct { - #[validate(length(equal = 11))] - val: CustomString - } + #[derive(Debug, Validate)] + struct EqualsTestStruct { + #[validate(length(equal = 11))] + val: CustomString, + } - let too_short = TestStruct { - val: CustomString(String::from("oops")) - }; + let too_short = TestStruct { val: CustomString(String::from("oops")) }; - let too_long = TestStruct { - val: CustomString(String::from("too long for this")) - }; + let too_long = TestStruct { val: CustomString(String::from("too long for this")) }; - let ok = TestStruct { - val: CustomString(String::from("perfect")) - }; + let ok = TestStruct { val: CustomString(String::from("perfect")) }; - let equals_ok = EqualsTestStruct { - val: CustomString(String::from("just enough")) - }; + let equals_ok = EqualsTestStruct { val: CustomString(String::from("just enough")) }; assert!(too_short.validate().is_err()); assert!(too_long.validate().is_err()); - assert!(ok.validate().is_ok()); - assert!(equals_ok.validate().is_ok()); -} \ No newline at end of file + assert!(ok.validate().is_ok()); + assert!(equals_ok.validate().is_ok()); +} diff --git a/validator_derive_tests/tests/required.rs b/validator_derive_tests/tests/required.rs index c9f751b1..222a49d5 100644 --- a/validator_derive_tests/tests/required.rs +++ b/validator_derive_tests/tests/required.rs @@ -119,4 +119,4 @@ fn can_validate_custom_impl_for_required() { assert!(something.validate().is_ok()); assert!(nothing.validate().is_err()); -} \ No newline at end of file +} From 897811aa7d95c8b98c9be25d099f516df6d3a956 Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 12 May 2023 21:46:38 -0400 Subject: [PATCH 4/8] Remove phone feature and code --- validator/Cargo.toml | 2 - validator/src/validation/mod.rs | 2 - validator/src/validation/phone.rs | 51 -------------------- validator_derive/Cargo.toml | 1 - validator_derive/src/lib.rs | 8 +--- validator_derive/src/quoting.rs | 22 --------- validator_derive/src/validation.rs | 4 +- validator_derive_tests/Cargo.toml | 2 +- validator_derive_tests/tests/phone.rs | 67 --------------------------- validator_types/Cargo.toml | 1 - 10 files changed, 3 insertions(+), 157 deletions(-) delete mode 100644 validator/src/validation/phone.rs delete mode 100644 validator_derive_tests/tests/phone.rs diff --git a/validator/Cargo.toml b/validator/Cargo.toml index 5c69bd17..212cf1e6 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -20,13 +20,11 @@ serde_derive = "1" serde_json = "1" validator_derive = { version = "0.16", path = "../validator_derive", optional = true } card-validate = { version = "2.2", optional = true } -phonenumber = { version = "0.3", optional = true } unic-ucd-common = { version = "0.9", optional = true } indexmap = {version = "1", features = ["serde-1"], optional = true } [features] -phone = ["phonenumber", "validator_derive/phone"] card = ["card-validate", "validator_derive/card"] unic = ["unic-ucd-common", "validator_derive/unic"] derive = ["validator_derive"] diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs index d4307153..28cb3762 100644 --- a/validator/src/validation/mod.rs +++ b/validator/src/validation/mod.rs @@ -8,8 +8,6 @@ pub mod length; pub mod must_match; #[cfg(feature = "unic")] pub mod non_control_character; -#[cfg(feature = "phone")] -pub mod phone; pub mod range; pub mod required; pub mod urls; diff --git a/validator/src/validation/phone.rs b/validator/src/validation/phone.rs deleted file mode 100644 index 96a81371..00000000 --- a/validator/src/validation/phone.rs +++ /dev/null @@ -1,51 +0,0 @@ -use std::borrow::Cow; - -#[must_use] -pub fn validate_phone<'a, T>(phone_number: T) -> bool -where - T: Into>, -{ - if let Ok(parsed) = phonenumber::parse(None, phone_number.into()) { - phonenumber::is_valid(&parsed) - } else { - false - } -} - -#[cfg(test)] -mod tests { - use std::borrow::Cow; - - use super::validate_phone; - - #[test] - fn test_phone() { - let tests = vec![ - ("+1 (415) 237-0800", true), - ("+14152370800", true), - ("+33642926829", true), - ("14152370800", false), - ("0642926829", false), - ("00642926829", false), - ("A012", false), - ("TEXT", false), - ]; - - for (input, expected) in tests { - println!("{} - {}", input, expected); - assert_eq!(validate_phone(input), expected); - } - } - - #[test] - fn test_phone_cow() { - let test: Cow<'static, str> = "+1 (415) 237-0800".into(); - assert!(validate_phone(test)); - let test: Cow<'static, str> = String::from("+1 (415) 237-0800").into(); - assert!(validate_phone(test)); - let test: Cow<'static, str> = "TEXT".into(); - assert!(!validate_phone(test)); - let test: Cow<'static, str> = String::from("TEXT").into(); - assert!(!validate_phone(test)); - } -} diff --git a/validator_derive/Cargo.toml b/validator_derive/Cargo.toml index 4666e7bd..a7d2201c 100644 --- a/validator_derive/Cargo.toml +++ b/validator_derive/Cargo.toml @@ -14,7 +14,6 @@ readme = "../README.md" proc-macro = true [features] -phone = ["validator_types/phone"] card = ["validator_types/card"] unic = ["validator_types/unic"] diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index 143fbd00..adf810c2 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -404,7 +404,7 @@ fn find_validators_for_field( for meta_item in meta_items { match *meta_item { syn::NestedMeta::Meta(ref item) => match *item { - // email, url, phone, credit_card, non_control_character + // email, url, credit_card, non_control_character syn::Meta::Path(ref name) => { match name.get_ident().unwrap().to_string().as_ref() { "email" => { @@ -413,11 +413,6 @@ fn find_validators_for_field( "url" => { validators.push(FieldValidation::new(Validator::Url)); } - #[cfg(feature = "phone")] - "phone" => { - assert_string_type("phone", field_type, &field.ty); - validators.push(FieldValidation::new(Validator::Phone)); - } #[cfg(feature = "card")] "credit_card" => { assert_string_type("credit_card", field_type, &field.ty); @@ -528,7 +523,6 @@ fn find_validators_for_field( } "email" | "url" - | "phone" | "credit_card" | "non_control_character" | "required" => { diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs index 3d0f5cca..443a5766 100644 --- a/validator_derive/src/quoting.rs +++ b/validator_derive/src/quoting.rs @@ -309,26 +309,6 @@ pub fn quote_credit_card_validation( field_quoter.wrap_if_option(quoted) } -#[cfg(feature = "phone")] -pub fn quote_phone_validation( - field_quoter: &FieldQuoter, - validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let field_name = &field_quoter.name; - let validator_param = field_quoter.quote_validator_param(); - - let quoted_error = quote_error(validation); - let quoted = quote!( - if !::validator::validate_phone(#validator_param) { - #quoted_error - err.add_param(::std::borrow::Cow::from("value"), &#validator_param); - errors.add(#field_name, err); - } - ); - - field_quoter.wrap_if_option(quoted) -} - #[cfg(feature = "unic")] pub fn quote_non_control_character_validation( field_quoter: &FieldQuoter, @@ -539,8 +519,6 @@ pub fn quote_validator( Validator::CreditCard => { validations.push(quote_credit_card_validation(field_quoter, validation)) } - #[cfg(feature = "phone")] - Validator::Phone => validations.push(quote_phone_validation(field_quoter, validation)), Validator::Nested => nested_validations.push(quote_nested_validation(field_quoter)), #[cfg(feature = "unic")] Validator::NonControlCharacter => { diff --git a/validator_derive/src/validation.rs b/validator_derive/src/validation.rs index a9397707..34d768d6 100644 --- a/validator_derive/src/validation.rs +++ b/validator_derive/src/validation.rs @@ -297,7 +297,7 @@ pub fn extract_custom_validation( } } -/// Extract url/email/phone/non_control_character field validation with a code or a message +/// Extract url/email/non_control_character field validation with a code or a message pub fn extract_argless_validation( validator_name: String, field: String, @@ -335,8 +335,6 @@ pub fn extract_argless_validation( "email" => Validator::Email, #[cfg(feature = "card")] "credit_card" => Validator::CreditCard, - #[cfg(feature = "phone")] - "phone" => Validator::Phone, #[cfg(feature = "unic")] "non_control_character" => Validator::NonControlCharacter, "required" => Validator::Required, diff --git a/validator_derive_tests/Cargo.toml b/validator_derive_tests/Cargo.toml index 56ca22cc..4e838750 100644 --- a/validator_derive_tests/Cargo.toml +++ b/validator_derive_tests/Cargo.toml @@ -5,7 +5,7 @@ authors = ["Vincent Prouillet "] edition = "2018" [dev-dependencies] -validator = { version = "0.16", path = "../validator", features = ["phone", "card", "unic", "derive", "indexmap"] } +validator = { version = "0.16", path = "../validator", features = ["card", "unic", "derive", "indexmap"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" trybuild = "1.0" diff --git a/validator_derive_tests/tests/phone.rs b/validator_derive_tests/tests/phone.rs deleted file mode 100644 index 1f3c9bea..00000000 --- a/validator_derive_tests/tests/phone.rs +++ /dev/null @@ -1,67 +0,0 @@ -use validator::Validate; - -#[test] -fn can_validate_phone_ok() { - #[derive(Debug, Validate)] - struct TestStruct { - #[validate(phone)] - val: String, - } - - let s = TestStruct { val: "+14152370800".to_string() }; - - assert!(s.validate().is_ok()); -} - -#[test] -fn bad_phone_fails_validation() { - #[derive(Debug, Validate)] - struct TestStruct { - #[validate(phone)] - val: String, - } - - let s = TestStruct { val: "bob".to_string() }; - let res = s.validate(); - assert!(res.is_err()); - let err = res.unwrap_err(); - let errs = err.field_errors(); - assert!(errs.contains_key("val")); - assert_eq!(errs["val"].len(), 1); - assert_eq!(errs["val"][0].code, "phone"); -} - -#[test] -fn can_specify_code_for_phone() { - #[derive(Debug, Validate)] - struct TestStruct { - #[validate(phone(code = "oops"))] - val: String, - } - let s = TestStruct { val: "bob".to_string() }; - let res = s.validate(); - assert!(res.is_err()); - let err = res.unwrap_err(); - let errs = err.field_errors(); - assert!(errs.contains_key("val")); - assert_eq!(errs["val"].len(), 1); - assert_eq!(errs["val"][0].code, "oops"); - assert_eq!(errs["val"][0].params["value"], "bob"); -} - -#[test] -fn can_specify_message_for_phone() { - #[derive(Debug, Validate)] - struct TestStruct { - #[validate(phone(message = "oops"))] - val: String, - } - let s = TestStruct { val: "bob".to_string() }; - let res = s.validate(); - assert!(res.is_err()); - let err = res.unwrap_err(); - let errs = err.field_errors(); - assert!(errs.contains_key("val")); - assert_eq!(errs["val"].len(), 1); - assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); -} diff --git a/validator_types/Cargo.toml b/validator_types/Cargo.toml index 8e4122c0..ed6c8523 100644 --- a/validator_types/Cargo.toml +++ b/validator_types/Cargo.toml @@ -12,7 +12,6 @@ readme = "../README.md" [features] -phone = [] card = [] unic = [] From cbca88e302901c83acaa09046e7ff7e2ec5feb6b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Thu, 5 Jan 2023 11:40:07 +0100 Subject: [PATCH 5/8] Add trait for inspecting all validation constraints of a type --- validator/src/lib.rs | 7 ++- validator/src/traits.rs | 76 ++++++++++++++++++++++- validator/src/types.rs | 134 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 214 insertions(+), 3 deletions(-) diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 4bbf3033..8670543d 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -86,8 +86,11 @@ pub use validation::range::{validate_range, ValidateRange}; pub use validation::required::{validate_required, ValidateRequired}; pub use validation::urls::{validate_url, ValidateUrl}; -pub use traits::{Contains, HasLen, Validate, ValidateArgs}; -pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind}; +pub use traits::{Constraints, Contains, HasLen, 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 05378bc6..a385d9d4 100644 --- a/validator/src/traits.rs +++ b/validator/src/traits.rs @@ -4,7 +4,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; #[cfg(feature = "indexmap")] use indexmap::{IndexMap, IndexSet}; -use crate::types::ValidationErrors; +use crate::types::{ValidationConstraints, ValidationErrors}; /// Trait to implement if one wants to make the `length` validator /// work for more types @@ -210,3 +210,77 @@ pub trait ValidateArgs<'v_a> { fn validate_args(&self, args: Self::Args) -> Result<(), ValidationErrors>; } + +/// Provides an associated function that returns all of the validator constraints applied to its +/// fields. +pub trait Constraints { + fn constraints() -> ValidationConstraints; +} + +impl Constraints for &T { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} + +impl Constraints for &[T] { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} + +impl Constraints for [T; N] { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} + +impl Constraints for Option { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} + +impl Constraints for Vec { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} + +impl Constraints for HashMap { + fn constraints() -> ValidationConstraints { + V::constraints() + } +} + +impl Constraints for HashSet { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} + +impl Constraints for BTreeMap { + fn constraints() -> ValidationConstraints { + V::constraints() + } +} + +impl Constraints for BTreeSet { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} + +#[cfg(feature = "indexmap")] +impl Constraints for IndexMap { + fn constraints() -> ValidationConstraints { + V::constraints() + } +} + +#[cfg(feature = "indexmap")] +impl Constraints for IndexSet { + fn constraints() -> ValidationConstraints { + T::constraints() + } +} diff --git a/validator/src/types.rs b/validator/src/types.rs index e8d1de2f..3e9e6459 100644 --- a/validator/src/types.rs +++ b/validator/src/types.rs @@ -175,3 +175,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 { + Email { + code: &'static str, + }, + Url { + code: &'static str, + }, + Custom { + function: &'static str, + code: &'static str, + }, + MustMatch { + other_field: &'static str, + code: &'static str, + }, + Contains { + needle: &'static str, + code: &'static str, + }, + DoesNotContain { + needle: &'static str, + code: &'static str, + }, + Regex { + name: &'static str, + code: &'static str, + }, + Range { + min: Option, + max: Option, + code: &'static str, + }, + Length { + length: LengthConstraint, + code: &'static str, + }, + #[cfg(feature = "card")] + CreditCard { + code: &'static str, + }, + Nested, + #[cfg(feature = "unic")] + NonControlCharacter { + code: &'static str, + }, + Required { + code: &'static str, + }, + RequiredNested { + code: &'static str, + }, +} + +impl ValidationConstraint { + pub fn code(&self) -> &'static str { + match *self { + Self::Email { code, .. } => code, + Self::Url { code, .. } => code, + Self::Custom { code, .. } => code, + Self::MustMatch { code, .. } => code, + Self::Contains { code, .. } => code, + Self::DoesNotContain { code, .. } => code, + Self::Regex { code, .. } => code, + Self::Range { code, .. } => code, + Self::Length { code, .. } => code, + #[cfg(feature = "card")] + Self::CreditCard { code, .. } => code, + Self::Nested => "nested", + #[cfg(feature = "unic")] + Self::NonControlCharacter { code, .. } => code, + Self::Required { code, .. } => code, + Self::RequiredNested { code, .. } => code, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize)] +#[serde(untagged)] +pub enum ValidationConstraintsKind { + Struct(Box), + Field(Vec), +} + +#[derive(Default, Debug, Clone, PartialEq, Serialize)] +pub struct ValidationConstraints(pub HashMap<&'static str, Vec>); + +impl ValidationConstraints { + pub fn new() -> Self { + Self(HashMap::new()) + } + + pub fn merge( + parent: &mut ValidationConstraints, + field: &'static str, + child: ValidationConstraints, + ) { + parent.add_nested(field, ValidationConstraintsKind::Struct(Box::new(child))); + } + + pub fn add(&mut self, field: &'static str, constraint: ValidationConstraint) { + let entry = self.0.entry(field).or_insert_with(|| Vec::new()); + + let kind = entry.iter_mut().find_map(|kind| match kind { + ValidationConstraintsKind::Field(field) => Some(field), + _ => None, + }); + match kind { + Some(field) => { + field.push(constraint); + } + None => { + entry.push(ValidationConstraintsKind::Field(vec![constraint])); + } + }; + } + + #[must_use] + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + fn add_nested(&mut self, field: &'static str, constraints: ValidationConstraintsKind) { + self.0.entry(field).or_insert_with(|| Vec::new()).push(constraints); + } +} From 803b7b1875c3826200eb9232734c408cb43e396e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Thu, 5 Jan 2023 12:06:12 +0100 Subject: [PATCH 6/8] Make `derive(Validate)` implement Constraints trait --- validator/src/traits.rs | 2 + validator_derive/src/lib.rs | 43 ++- validator_derive/src/quoting.rs | 302 +++++++++++++++--- .../custom/validate_not_impl_with_args.stderr | 4 +- .../compile-fail/no_nested_validations.stderr | 20 +- 5 files changed, 310 insertions(+), 61 deletions(-) diff --git a/validator/src/traits.rs b/validator/src/traits.rs index a385d9d4..2179f818 100644 --- a/validator/src/traits.rs +++ b/validator/src/traits.rs @@ -213,6 +213,8 @@ pub trait ValidateArgs<'v_a> { /// Provides an associated function that returns all of the validator constraints applied to its /// fields. +/// +/// This trait is implemented by deriving `Validate`. pub trait Constraints { fn constraints() -> ValidationConstraints; } diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index adf810c2..8581cbc3 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -35,7 +35,8 @@ fn impl_validate(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { let mut struct_validations = find_struct_validations(&ast.attrs); let (arg_type, has_arg) = construct_validator_argument_type(&mut fields_validations, &mut struct_validations); - let (validations, nested_validations) = quote_field_validations(fields_validations); + let (validations, nested_validations, constraints, nested_constraints) = + quote_field_validations(fields_validations); let schema_validations = quote_schema_validations(&struct_validations); @@ -57,6 +58,22 @@ fn impl_validate(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { quote!() }; + let constraints_trait_impl = quote!( + impl #impl_generics ::validator::Constraints for #ident #ty_generics #where_clause { + #[allow(unused_mut)] + #[allow(unused_variable)] + fn constraints() -> ::validator::ValidationConstraints { + let mut constraints = ::validator::ValidationConstraints::new(); + + #(#constraints)* + + #(#nested_constraints)* + + constraints + } + } + ); + // Adding the validator lifetime 'v_a let mut expanded_generic = ast.generics.clone(); expanded_generic @@ -68,6 +85,7 @@ fn impl_validate(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { // Implementing ValidateArgs let impl_ast = quote!( #validate_trait_impl + #constraints_trait_impl // We need this here to prevent formatting lints that can be caused by `quote_spanned!` // See: rust-lang/rust-clippy#6249 for more reference @@ -191,20 +209,33 @@ fn construct_validator_argument_type( fn quote_field_validations( mut fields: Vec, -) -> (Vec, Vec) { +) -> ( + Vec, + Vec, + Vec, + Vec, +) { let mut validations = vec![]; let mut nested_validations = vec![]; + let mut constraints = vec![]; + let mut nested_constraints = vec![]; fields.drain(..).for_each(|x| { - let field_ident = x.field.ident.clone().unwrap(); - let field_quoter = FieldQuoter::new(field_ident, x.name, x.field_type); + let field_quoter = FieldQuoter::new(x.field, x.name, x.field_type); for validation in &x.validations { - quote_validator(&field_quoter, validation, &mut validations, &mut nested_validations); + quote_validator( + &field_quoter, + validation, + &mut validations, + &mut nested_validations, + &mut constraints, + &mut nested_constraints, + ); } }); - (validations, nested_validations) + (validations, nested_validations, constraints, nested_constraints) } /// Find if a struct has some schema validation and returns the info if so diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs index 443a5766..19315e86 100644 --- a/validator_derive/src/quoting.rs +++ b/validator_derive/src/quoting.rs @@ -11,7 +11,7 @@ use crate::validation::{FieldValidation, SchemaValidation}; /// Pass around all the information needed for creating a validation #[derive(Debug)] pub struct FieldQuoter { - ident: syn::Ident, + field: syn::Field, /// The field name name: String, /// The field type @@ -19,8 +19,8 @@ pub struct FieldQuoter { } impl FieldQuoter { - pub fn new(ident: syn::Ident, name: String, _type: String) -> FieldQuoter { - FieldQuoter { ident, name, _type } + pub fn new(field: syn::Field, name: String, _type: String) -> FieldQuoter { + FieldQuoter { field, name, _type } } /// Don't put a & in front a pointer since we are going to pass @@ -28,7 +28,7 @@ impl FieldQuoter { /// Also just use the ident without if it's optional and will go through /// a if let first pub fn quote_validator_param(&self) -> proc_macro2::TokenStream { - let ident = &self.ident; + let ident = self.field.ident.as_ref().unwrap(); if self._type.starts_with("Option<") { quote!(#ident) @@ -42,7 +42,7 @@ impl FieldQuoter { } pub fn quote_validator_field(&self) -> proc_macro2::TokenStream { - let ident = &self.ident; + let ident = self.field.ident.as_ref().unwrap(); if self._type.starts_with("Option<") || is_list(&self._type) || is_map(&self._type) { quote!(#ident) @@ -54,7 +54,7 @@ impl FieldQuoter { } pub fn get_optional_validator_param(&self) -> proc_macro2::TokenStream { - let ident = &self.ident; + let ident = self.field.ident.as_ref().unwrap(); if self._type.starts_with("Option<&") || self._type.starts_with("Option proc_macro2::TokenStream { - let field_ident = &self.ident; + let field_ident = self.field.ident.as_ref().unwrap(); let optional_pattern_matched = self.get_optional_validator_param(); if self._type.starts_with("Option proc_macro2::TokenStream { - let field_ident = &self.ident; + let field_ident = self.field.ident.as_ref().unwrap(); let field_name = &self.name; // When we're using an option, we'll have the field unwrapped, so we should not access it @@ -166,7 +166,7 @@ fn quote_error(validation: &FieldValidation) -> proc_macro2::TokenStream { pub fn quote_length_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -196,9 +196,12 @@ pub fn quote_length_validation( let max_tokens = option_to_tokens( &max.clone().as_ref().map(value_or_path_to_tokens).map(|x| quote!(#x as u64)), ); - let equal_tokens = option_to_tokens( - &equal.clone().as_ref().map(value_or_path_to_tokens).map(|x| quote!(#x as u64)), - ); + let (option_equal, equal_tokens) = { + let option_token = + equal.clone().as_ref().map(value_or_path_to_tokens).map(|x| quote!(#x as u64)); + let tokens = option_to_tokens(&option_token); + (option_token, tokens) + }; let quoted_error = quote_error(validation); let quoted = quote!( @@ -217,7 +220,36 @@ pub fn quote_length_validation( } ); - return field_quoter.wrap_if_option(quoted); + let code = &validation.code; + let constraint_quoted = match option_equal { + Some(equal) => { + quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Length { + length: ::validator::LengthConstraint::Equal(#equal), + code: #code, + }, + ); + ) + } + None => { + quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Length { + length: ::validator::LengthConstraint::Range { + min: #min_tokens, + max: #max_tokens, + }, + code: #code, + }, + ); + ) + } + }; + + return (field_quoter.wrap_if_option(quoted), constraint_quoted); } unreachable!() @@ -226,7 +258,7 @@ pub fn quote_length_validation( pub fn quote_range_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let quoted_ident = field_quoter.quote_validator_param(); @@ -263,7 +295,19 @@ pub fn quote_range_validation( } ); - return field_quoter.wrap_if_option(quoted); + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Range { + min: #min_tokens, + max: #max_tokens, + code: #code, + }, + ); + ); + + return (field_quoter.wrap_if_option(quoted), constraint_quoted); } unreachable!() @@ -293,7 +337,7 @@ where pub fn quote_credit_card_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -306,14 +350,24 @@ pub fn quote_credit_card_validation( } ); - field_quoter.wrap_if_option(quoted) + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::CreditCard { + code: #code, + }, + ); + ); + + (field_quoter.wrap_if_option(quoted), constraint_quoted) } #[cfg(feature = "unic")] pub fn quote_non_control_character_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -326,13 +380,23 @@ pub fn quote_non_control_character_validation( } ); - field_quoter.wrap_if_option(quoted) + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::NonControlCharacter { + code: #code, + }, + ); + ); + + (field_quoter.wrap_if_option(quoted), constraint_quoted) } pub fn quote_url_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -345,13 +409,23 @@ pub fn quote_url_validation( } ); - field_quoter.wrap_if_option(quoted) + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Url { + code: #code, + }, + ); + ); + + (field_quoter.wrap_if_option(quoted), constraint_quoted) } pub fn quote_email_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -364,14 +438,24 @@ pub fn quote_email_validation( } ); - field_quoter.wrap_if_option(quoted) + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Email { + code: #code, + }, + ); + ); + + (field_quoter.wrap_if_option(quoted), constraint_quoted) } pub fn quote_must_match_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { - let ident = &field_quoter.ident; +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { + let ident = field_quoter.field.ident.as_ref().unwrap(); let field_name = &field_quoter.name; if let Validator::MustMatch(ref other) = validation.validator { @@ -386,7 +470,19 @@ pub fn quote_must_match_validation( } ); - return field_quoter.wrap_if_option(quoted); + let other_ident_string = other_ident.to_string(); + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::MustMatch { + other_field: #other_ident_string, + code: #code, + }, + ); + ); + + return (field_quoter.wrap_if_option(quoted), constraint_quoted); } unreachable!(); @@ -395,7 +491,7 @@ pub fn quote_must_match_validation( pub fn quote_custom_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -429,7 +525,18 @@ pub fn quote_custom_validation( }; ); - return field_quoter.wrap_if_option(quoted); + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Custom { + function: #function, + code: #code, + }, + ); + ); + + return (field_quoter.wrap_if_option(quoted), constraint_quoted); } unreachable!(); @@ -438,7 +545,7 @@ pub fn quote_custom_validation( pub fn quote_contains_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -453,7 +560,18 @@ pub fn quote_contains_validation( } ); - return field_quoter.wrap_if_option(quoted); + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Contains { + needle: #needle, + code: #code, + }, + ); + ); + + return (field_quoter.wrap_if_option(quoted), constraint_quoted); } unreachable!(); @@ -462,7 +580,7 @@ pub fn quote_contains_validation( pub fn quote_regex_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -477,17 +595,38 @@ pub fn quote_regex_validation( } ); - return field_quoter.wrap_if_option(quoted); + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Regex { + name: #re, + code: #code, + }, + ); + ); + + return (field_quoter.wrap_if_option(quoted), constraint_quoted); } unreachable!(); } -pub fn quote_nested_validation(field_quoter: &FieldQuoter) -> proc_macro2::TokenStream { +pub fn quote_nested_validation( + field_quoter: &FieldQuoter, +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_field = field_quoter.quote_validator_field(); let quoted = quote!(result = ::validator::ValidationErrors::merge(result, #field_name, #validator_field.validate());); - field_quoter.wrap_if_option(field_quoter.wrap_if_collection(quoted)) + + let ty = &field_quoter.field.ty; + let constraints_quoted = quote!( + ::validator::ValidationConstraints::merge( + &mut constraints, #field_name, <#ty as ::validator::Constraints>::constraints(), + ); + ); + + (field_quoter.wrap_if_option(field_quoter.wrap_if_collection(quoted)), constraints_quoted) } pub fn quote_validator( @@ -495,40 +634,78 @@ pub fn quote_validator( validation: &FieldValidation, validations: &mut Vec, nested_validations: &mut Vec, + constraints: &mut Vec, + nested_constraints: &mut Vec, ) { match validation.validator { Validator::Length { .. } => { - validations.push(quote_length_validation(field_quoter, validation)) + let (validation, constraint) = quote_length_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } Validator::Range { .. } => { - validations.push(quote_range_validation(field_quoter, validation)) + let (validation, constraint) = quote_range_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); + } + Validator::Email => { + let (validation, constraint) = quote_email_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); + } + Validator::Url => { + let (validation, constraint) = quote_url_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } - Validator::Email => validations.push(quote_email_validation(field_quoter, validation)), - Validator::Url => validations.push(quote_url_validation(field_quoter, validation)), Validator::MustMatch(_) => { - validations.push(quote_must_match_validation(field_quoter, validation)) + let (validation, constraint) = quote_must_match_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } Validator::Custom { .. } => { - validations.push(quote_custom_validation(field_quoter, validation)) + let (validation, constraint) = quote_custom_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } Validator::Contains(_) => { - validations.push(quote_contains_validation(field_quoter, validation)) + let (validation, constraint) = quote_contains_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); + } + Validator::Regex(_) => { + let (validation, constraint) = quote_regex_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } - Validator::Regex(_) => validations.push(quote_regex_validation(field_quoter, validation)), #[cfg(feature = "card")] Validator::CreditCard => { - validations.push(quote_credit_card_validation(field_quoter, validation)) + let (validation, constraint) = quote_credit_card_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); + } + Validator::Nested => { + let (validation, constraint) = quote_nested_validation(field_quoter); + nested_validations.push(validation); + nested_constraints.push(constraint); } - Validator::Nested => nested_validations.push(quote_nested_validation(field_quoter)), #[cfg(feature = "unic")] Validator::NonControlCharacter => { - validations.push(quote_non_control_character_validation(field_quoter, validation)) + let (validation, constraint) = + quote_non_control_character_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } Validator::Required | Validator::RequiredNested => { - validations.push(quote_required_validation(field_quoter, validation)) + let (validation, constraint) = quote_required_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } Validator::DoesNotContain(_) => { - validations.push(quote_does_not_contain_validation(field_quoter, validation)) + let (validation, constraint) = + quote_does_not_contain_validation(field_quoter, validation); + validations.push(validation); + constraints.push(constraint); } } } @@ -577,9 +754,9 @@ pub fn quote_schema_validations(validation: &[SchemaValidation]) -> Vec proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; - let ident = &field_quoter.ident; + let ident = &field_quoter.field.ident.as_ref().unwrap(); let validator_param = quote!(&self.#ident); let quoted_error = quote_error(validation); @@ -591,13 +768,23 @@ pub fn quote_required_validation( } ); - quoted + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::Required { + code: #code, + }, + ); + ); + + (quoted, constraint_quoted) } pub fn quote_does_not_contain_validation( field_quoter: &FieldQuoter, validation: &FieldValidation, -) -> proc_macro2::TokenStream { +) -> (proc_macro2::TokenStream, proc_macro2::TokenStream) { let field_name = &field_quoter.name; let validator_param = field_quoter.quote_validator_param(); @@ -612,7 +799,18 @@ pub fn quote_does_not_contain_validation( } ); - return field_quoter.wrap_if_option(quoted); + let code = &validation.code; + let constraint_quoted = quote!( + constraints.add( + #field_name, + ::validator::ValidationConstraint::DoesNotContain { + needle: #needle, + code: #code, + }, + ); + ); + + return (field_quoter.wrap_if_option(quoted), constraint_quoted); } unreachable!(); diff --git a/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr b/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr index 960215cb..04db8d36 100644 --- a/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr +++ b/validator_derive_tests/tests/compile-fail/custom/validate_not_impl_with_args.stderr @@ -1,8 +1,8 @@ error[E0599]: no method named `validate` found for struct `Test` in the current scope - --> $DIR/validate_not_impl_with_args.rs:15:10 + --> tests/compile-fail/custom/validate_not_impl_with_args.rs:15:10 | 8 | struct Test { - | ----------- method `validate` not found for this + | ----------- method `validate` not found for this struct ... 15 | test.validate(); | ^^^^^^^^ method not found in `Test` 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 f33ce787..aa69f76e 100644 --- a/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr +++ b/validator_derive_tests/tests/compile-fail/no_nested_validations.stderr @@ -1,3 +1,21 @@ +error[E0277]: the trait bound `Nested: Constraints` is not satisfied + --> tests/compile-fail/no_nested_validations.rs:3:10 + | +3 | #[derive(Validate)] + | ^^^^^^^^ the trait `Constraints` is not implemented for `Nested` + | + = help: the following other types implement trait `Constraints`: + &T + &[T] + BTreeMap + BTreeSet + HashMap + HashSet + Option + Test + and $N others + = note: this error originates in the derive macro `Validate` (in Nightly builds, run with -Z macro-backtrace for more info) + error[E0599]: no method named `validate` found for struct `Nested` in the current scope --> tests/compile-fail/no_nested_validations.rs:3:10 | @@ -5,7 +23,7 @@ error[E0599]: no method named `validate` found for struct `Nested` in the curren | ^^^^^^^^ method not found in `Nested` ... 9 | struct Nested { - | ------------- method `validate` not found for this + | ------------- method `validate` not found for this struct | = help: items from traits can only be used if the trait is implemented and in scope = note: the following trait defines an item `validate`, perhaps you need to implement it: From ecde4f43dc2fc46da4c1a7e1269668e4695401c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2023 10:48:33 +0100 Subject: [PATCH 7/8] Make collection types wrap the constraints in a different constraint kind This makes it possible for code consuming the constraints to know whether the type is used directly or if it is held in some kind of collection. --- validator/src/traits.rs | 40 +++++++++++++++++++++++++++++++++ validator/src/types.rs | 12 +++++++--- validator_derive/src/quoting.rs | 5 ++++- 3 files changed, 53 insertions(+), 4 deletions(-) diff --git a/validator/src/traits.rs b/validator/src/traits.rs index 2179f818..ed1e63ce 100644 --- a/validator/src/traits.rs +++ b/validator/src/traits.rs @@ -217,6 +217,10 @@ pub trait ValidateArgs<'v_a> { /// This trait is implemented by deriving `Validate`. pub trait Constraints { fn constraints() -> ValidationConstraints; + + fn is_collection() -> bool { + false + } } impl Constraints for &T { @@ -229,12 +233,20 @@ impl Constraints for &[T] { fn constraints() -> ValidationConstraints { T::constraints() } + + fn is_collection() -> bool { + true + } } impl Constraints for [T; N] { fn constraints() -> ValidationConstraints { T::constraints() } + + fn is_collection() -> bool { + true + } } impl Constraints for Option { @@ -247,30 +259,50 @@ impl Constraints for Vec { 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 { T::constraints() } + + fn is_collection() -> bool { + true + } } impl Constraints for BTreeMap { fn constraints() -> ValidationConstraints { V::constraints() } + + fn is_collection() -> bool { + true + } } impl Constraints for BTreeSet { fn constraints() -> ValidationConstraints { T::constraints() } + + fn is_collection() -> bool { + true + } } #[cfg(feature = "indexmap")] @@ -278,6 +310,10 @@ impl Constraints for IndexMap { fn constraints() -> ValidationConstraints { V::constraints() } + + fn is_collection() -> bool { + true + } } #[cfg(feature = "indexmap")] @@ -285,4 +321,8 @@ impl Constraints for IndexSet { fn constraints() -> ValidationConstraints { T::constraints() } + + fn is_collection() -> bool { + true + } } diff --git a/validator/src/types.rs b/validator/src/types.rs index 3e9e6459..dd206810 100644 --- a/validator/src/types.rs +++ b/validator/src/types.rs @@ -264,6 +264,7 @@ impl ValidationConstraint { #[serde(untagged)] pub enum ValidationConstraintsKind { Struct(Box), + Collection(Box), Field(Vec), } @@ -279,12 +280,17 @@ impl ValidationConstraints { parent: &mut ValidationConstraints, field: &'static str, child: ValidationConstraints, + is_collection: bool, ) { - parent.add_nested(field, ValidationConstraintsKind::Struct(Box::new(child))); + if is_collection { + parent.add_nested(field, ValidationConstraintsKind::Collection(Box::new(child))); + } else { + parent.add_nested(field, ValidationConstraintsKind::Struct(Box::new(child))); + } } pub fn add(&mut self, field: &'static str, constraint: ValidationConstraint) { - let entry = self.0.entry(field).or_insert_with(|| Vec::new()); + let entry = self.0.entry(field).or_insert_with(Vec::new); let kind = entry.iter_mut().find_map(|kind| match kind { ValidationConstraintsKind::Field(field) => Some(field), @@ -306,6 +312,6 @@ impl ValidationConstraints { } fn add_nested(&mut self, field: &'static str, constraints: ValidationConstraintsKind) { - self.0.entry(field).or_insert_with(|| Vec::new()).push(constraints); + self.0.entry(field).or_insert_with(Vec::new).push(constraints); } } diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs index 19315e86..6c0ac2c9 100644 --- a/validator_derive/src/quoting.rs +++ b/validator_derive/src/quoting.rs @@ -622,7 +622,10 @@ pub fn quote_nested_validation( let ty = &field_quoter.field.ty; let constraints_quoted = quote!( ::validator::ValidationConstraints::merge( - &mut constraints, #field_name, <#ty as ::validator::Constraints>::constraints(), + &mut constraints, + #field_name, + <#ty as ::validator::Constraints>::constraints(), + <#ty as ::validator::Constraints>::is_collection(), ); ); From 92b144d9d5eddada08bcd7b1514273cce78d5bb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20L=C3=B6thberg?= Date: Tue, 17 Jan 2023 15:18:19 +0100 Subject: [PATCH 8/8] Add tests for constaints trait --- validator_derive_tests/tests/constraints.rs | 83 +++++++++++++++++++++ 1 file changed, 83 insertions(+) create mode 100644 validator_derive_tests/tests/constraints.rs diff --git a/validator_derive_tests/tests/constraints.rs b/validator_derive_tests/tests/constraints.rs new file mode 100644 index 00000000..30d9afec --- /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] + 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" + } + ); + } + + { + 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" + } + ); +}