-
Notifications
You must be signed in to change notification settings - Fork 144
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
Trait validation #225
Changes from 12 commits
babd938
02bdd69
5836898
bd9e89f
8bd86e8
de20a94
ee04dd0
a77bdc9
0790ded
3277682
66380b6
34c12ee
ba0e745
090abc5
63559f8
9aeb298
77520dc
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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 | ||
|
@@ -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; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That implies allocation, it should return a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Returning a
How about returning Cow? There was a problem hiding this comment. Choose a reason for hiding this commentThe 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; | ||
|
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}; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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() { | ||
|
@@ -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))); | ||
} | ||
} |
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() | ||
} | ||
} |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?