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
Merged
6 changes: 3 additions & 3 deletions validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,17 +73,17 @@ 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;
Copy link
Owner

Choose a reason for hiding this comment

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

I'm tempted to remove that one from validator, it's currently too generic to be easily usable and can easily be added manually if needed.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Maybe leaving an example on how to add it with a custom validator after removing it would be a good idea?

pub use validation::range::validate_range;

pub use validation::required::validate_required;
pub use validation::required::{validate_required, ValidateRequired};
pub use validation::urls::validate_url;

pub use traits::{Contains, HasLen, Validate, ValidateArgs};
Expand Down
97 changes: 64 additions & 33 deletions validator/src/validation/email.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Cow<'a, str>>,
{
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<T: ValidateEmail>(val: T) -> bool {
val.validate_email()
}

/// Checks if the domain is a valid domain and if not, check whether it's an IP
Expand All @@ -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(&self) -> String;
Copy link
Owner

Choose a reason for hiding this comment

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

That implies allocation, it should return a &str instead

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Returning a &str is impossible if I want to return a formatted string for example

fn to_email_string(&self) -> &str {
    format!("{}@{}", self.user_part, self.domain_part).as_str()
}

How about returning Cow?

Copy link
Owner

Choose a reason for hiding this comment

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

Cow is fine then

}

impl ValidateEmail for &str {
fn to_email_string(&self) -> String {
self.to_string()
}
}

impl ValidateEmail for String {
fn to_email_string(&self) -> String {
self.to_string()
}
}

impl ValidateEmail for &String {
fn to_email_string(&self) -> String {
self.to_string()
}
}

impl<'a> ValidateEmail for Cow<'a, str> {
fn to_email_string(&self) -> String {
self.to_string()
}
}

#[cfg(test)]
mod tests {
use std::borrow::Cow;
Expand Down
202 changes: 182 additions & 20 deletions validator/src/validation/length.rs
Original file line number Diff line number Diff line change
@@ -1,42 +1,164 @@
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();

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<u64>, max: Option<u64>, equal: Option<u64>) -> 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<T> ValidateLength for Vec<T> {
fn length(&self) -> u64 {
self.len() as u64
}
}

impl<'a, T> ValidateLength for &'a Vec<T> {
fn length(&self) -> u64 {
self.len() as u64
}
}

impl<T> ValidateLength for &[T] {
fn length(&self) -> u64 {
self.len() as u64
}
}

impl<T, const N: usize> ValidateLength for [T; N] {
fn length(&self) -> u64 {
N as u64
}
}

impl<T, const N: usize> ValidateLength for &[T; N] {
fn length(&self) -> u64 {
N as u64
}
}

impl<'a, K, V, S> ValidateLength for &'a HashMap<K, V, S> {
fn length(&self) -> u64 {
self.len() as u64
}
}

impl<K, V, S> ValidateLength for HashMap<K, V, S> {
fn length(&self) -> u64 {
self.len() as u64
}
}

impl<'a, T, S> ValidateLength for &'a HashSet<T, S> {
fn length(&self) -> u64 {
self.len() as u64
}
}

impl<'a, K, V> ValidateLength for &'a BTreeMap<K, V> {
fn length(&self) -> u64 {
self.len() as u64
}
}

true
impl<'a, T> ValidateLength for &'a BTreeSet<T> {
fn length(&self) -> u64 {
self.len() as u64
}
}

impl<T> ValidateLength for BTreeSet<T> {
fn length(&self) -> u64 {
self.len() as u64
}
}

#[cfg(feature = "indexmap")]
impl<'a, K, V> ValidateLength for &'a IndexMap<K, V> {
fn length(&self) -> u64 {
self.len() as u64
}
}

#[cfg(feature = "indexmap")]
impl<'a, T> ValidateLength for &'a IndexSet<T> {
fn length(&self) -> u64 {
self.len() as u64
}
}

#[cfg(feature = "indexmap")]
impl<T> ValidateLength for IndexSet<T> {
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() {
Expand Down Expand Up @@ -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)));
}
}
16 changes: 15 additions & 1 deletion validator/src/validation/required.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
/// Validates whether the given Option is Some
#[must_use]
pub fn validate_required<T>(val: &Option<T>) -> bool {
pub fn validate_required<T: ValidateRequired>(val: &T) -> bool {
val.is_some()
}

pub trait ValidateRequired {
fn validate_required(&self) -> bool {
self.is_some()
}

fn is_some(&self) -> bool;
}

impl<T> ValidateRequired for Option<T> {
fn is_some(&self) -> bool {
self.is_some()
}
}
Loading