Skip to content

Commit

Permalink
Trait validation (#225)
Browse files Browse the repository at this point in the history
* 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 a77bdc9.

* 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č <tilen.pintaric@aviko.si>
  • Loading branch information
2 people authored and Keats committed Mar 4, 2024
1 parent 5dec97c commit 3f373b6
Show file tree
Hide file tree
Showing 11 changed files with 520 additions and 76 deletions.
10 changes: 5 additions & 5 deletions validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
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<'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;
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};

/// 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)));
}
}
Loading

0 comments on commit 3f373b6

Please sign in to comment.