Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Trait validation #225

Merged
merged 17 commits into from
Apr 14, 2023
2 changes: 1 addition & 1 deletion validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ 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::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;
Expand Down
179 changes: 172 additions & 7 deletions validator/src/validation/length.rs
Original file line number Diff line number Diff line change
@@ -1,29 +1,39 @@
use crate::traits::HasLen;
use std::{borrow::Cow, collections::{HashMap, HashSet, BTreeMap, BTreeSet}};

#[cfg(feature = "indexmap")]
use indexmap::{IndexMap, IndexSet};
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thoughts on letting crates handle validator support themselves like they do for serde?


/// Validates the length of the value given.
/// If the validator has `equal` set, it will ignore any `min` and `max` value.
///
/// 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<T: HasLen>(
pub fn validate_length<T: ValidateLength>(
value: T,
min: Option<u64>,
max: Option<u64>,
equal: Option<u64>,
) -> bool {
let val_length = value.length();
value.validate_length(min, max, equal)
}

fn validate_length_for_trait(
length: u64,
min: Option<u64>,
max: Option<u64>,
equal: Option<u64>,
) -> bool {
if let Some(eq) = equal {
return val_length == eq;
return length == eq;
} else {
if let Some(m) = min {
if val_length < m {
if length < m {
return false;
}
}
if let Some(m) = max {
if val_length > m {
if length > m {
return false;
}
}
Expand All @@ -32,11 +42,126 @@ pub fn validate_length<T: HasLen>(
true
}

pub trait ValidateLength {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool;
}

impl ValidateLength for String {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.chars().count() as u64, min, max, equal)
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What made you decide to go with self.chars().count() instead of self.len()?
And why use u64 everywhere instead of usize?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used self.chars().count() because the HasLen trait used them too and I didn't really think about it. The same is for u64 instead of usize. The validate_length function used Option<u64>. I can refactor the trait to use self.len() and usize if it makes any difference.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strings should use self.chars().count()

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Keats interesting, what is the advantage of that over .len()?

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fn main() {
    let txt = "日本語";
    println!("len {:?} vs chars {:?}", txt.len(), txt.chars().count());
}

will print

len 9 vs chars 3

In this case no one reasonable will think of len as number of bytes used (.len()) vs number of characters, which is what people think of when they think of a len for a string.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wow very interesting! Makes sense to use chars count then

}
}

impl<'a> ValidateLength for &'a String {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.chars().count() as u64, min, max, equal)
}
}

impl<'a> ValidateLength for &'a str {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.chars().count() as u64, min, max, equal)
}
}

impl<'a> ValidateLength for Cow<'a, str> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.chars().count() as u64, min, max, equal)
}
}

impl<T> ValidateLength for Vec<T> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<'a, T> ValidateLength for &'a Vec<T> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<T> ValidateLength for &[T] {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<T, const N: usize> ValidateLength for [T; N] {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(N as u64, min, max, equal)
}
}

impl<T, const N: usize> ValidateLength for &[T; N] {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(N as u64, min, max, equal)
}
}

impl<'a, K, V, S> ValidateLength for &'a HashMap<K, V, S> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<K, V, S> ValidateLength for HashMap<K, V, S> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<'a, T, S> ValidateLength for &'a HashSet<T, S> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<'a, K, V> ValidateLength for &'a BTreeMap<K, V> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<'a, T> ValidateLength for &'a BTreeSet<T> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

impl<T> ValidateLength for BTreeSet<T> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

#[cfg(feature = "indexmap")]
impl<'a, K, V> ValidateLength for &'a IndexMap<K, V> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

#[cfg(feature = "indexmap")]
impl<'a, T> ValidateLength for &'a IndexSet<T> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

#[cfg(feature = "indexmap")]
impl<T> ValidateLength for IndexSet<T> {
fn validate_length(&self, min: Option<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
validate_length_for_trait(self.len() as u64, min, max, equal)
}
}

#[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() {
Expand Down Expand Up @@ -76,4 +201,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)));
}
}
64 changes: 64 additions & 0 deletions validator_derive_tests/tests/length.rs
Original file line number Diff line number Diff line change
Expand Up @@ -222,3 +222,67 @@ 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<u64>, max: Option<u64>, equal: Option<u64>) -> bool {
let length = self.0.chars().count() as u64;

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
}
}

#[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());
}