Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,8 +80,11 @@ pub use validation::regex::ValidateRegex;
pub use validation::required::ValidateRequired;
pub use validation::urls::ValidateUrl;

pub use traits::{Validate, ValidateArgs};
pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind};
pub use traits::{Constraints, Validate, ValidateArgs};
pub use types::{
LengthConstraint, ValidationConstraint, ValidationConstraints, ValidationConstraintsKind,
ValidationError, ValidationErrors, ValidationErrorsKind,
};

#[cfg(feature = "derive")]
pub use validator_derive::Validate;
120 changes: 117 additions & 3 deletions validator/src/traits.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
use crate::types::{ValidationErrors, ValidationErrorsKind};
use std::collections::btree_map::BTreeMap;
use std::collections::HashMap;
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet};

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

use crate::{
types::{ValidationErrors, ValidationErrorsKind},
ValidationConstraints,
};

/// This is the original trait that was implemented by deriving `Validate`. It will still be
/// implemented for struct validations that don't take custom arguments. The call is being
Expand Down Expand Up @@ -135,3 +141,111 @@ where
}
}
}

pub trait Constraints {
fn constraints() -> ValidationConstraints;

fn is_collection() -> bool {
false
}
}

impl<T: Constraints> Constraints for &T {
fn constraints() -> ValidationConstraints {
T::constraints()
}

fn is_collection() -> bool {
T::is_collection()
}
}

impl<T: Constraints> Constraints for Option<T> {
fn constraints() -> ValidationConstraints {
T::constraints()
}

fn is_collection() -> bool {
T::is_collection()
}
}

macro_rules! impl_constraints {
($ty:ty) => {
impl<T: Constraints> Constraints for $ty {
fn constraints() -> ValidationConstraints {
T::constraints()
}

fn is_collection() -> bool {
true
}
}
};
}

impl_constraints!(&[T]);
impl_constraints!(Vec<T>);
impl_constraints!(BTreeSet<T>);

impl<T: Constraints, const N: usize> Constraints for [T; N] {
fn constraints() -> ValidationConstraints {
T::constraints()
}

fn is_collection() -> bool {
true
}
}

impl<K, V: Constraints, S> Constraints for HashMap<K, V, S> {
fn constraints() -> ValidationConstraints {
V::constraints()
}

fn is_collection() -> bool {
true
}
}

impl<V: Constraints, S> Constraints for HashSet<V, S> {
fn constraints() -> ValidationConstraints {
V::constraints()
}

fn is_collection() -> bool {
true
}
}

impl<K, V: Constraints> Constraints for BTreeMap<K, V> {
fn constraints() -> ValidationConstraints {
V::constraints()
}

fn is_collection() -> bool {
true
}
}

#[cfg(feature = "indexmap")]
impl<K, V: Constraints> Constraints for IndexMap<K, V> {
fn constraints() -> ValidationConstraints {
V::constraints()
}

fn is_collection() -> bool {
true
}
}

#[cfg(feature = "indexmap")]
impl<V: Constraints> Constraints for IndexSet<V> {
fn constraints() -> ValidationConstraints {
V::constraints()
}

fn is_collection() -> bool {
true
}
}
134 changes: 134 additions & 0 deletions validator/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -205,3 +205,137 @@ impl std::error::Error for ValidationErrors {
None
}
}

#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub enum LengthConstraint {
Range { min: Option<u64>, max: Option<u64> },
Equal(u64),
}

#[derive(Debug, Clone, PartialEq, Serialize)]
#[non_exhaustive]
pub enum ValidationConstraint {
CreditCard {
code: Cow<'static, str>,
},
Contains {
code: Cow<'static, str>,
pattern: Cow<'static, str>,
},
DoesNotContain {
code: Cow<'static, str>,
pattern: Cow<'static, str>,
},
Email {
code: Cow<'static, str>,
},
Ip {
code: Cow<'static, str>,
v4: bool,
v6: bool,
},
Length {
code: Cow<'static, str>,
length: LengthConstraint,
},
MustMatch {
code: Cow<'static, str>,
other: Cow<'static, str>,
},
Nested,
NonControlCharacter {
code: Cow<'static, str>,
},
Range {
code: Cow<'static, str>,
min: Option<Cow<'static, str>>,
max: Option<Cow<'static, str>>,
exclusive_min: Option<Cow<'static, str>>,
exclusive_max: Option<Cow<'static, str>>,
},
Regex {
code: Cow<'static, str>,
path: Cow<'static, str>,
},
Required {
code: Cow<'static, str>,
},
RequiredNested {
code: Cow<'static, str>,
},
Url {
code: Cow<'static, str>,
},
Custom {
code: Option<Cow<'static, str>>,
function: Cow<'static, str>,
},
}

impl ValidationConstraint {
pub fn code(&self) -> &str {
match self {
Self::CreditCard { code, .. } => &code,
Self::Contains { code, .. } => &code,
Self::Custom { code, .. } => code.as_deref().unwrap_or("custom"),
Self::DoesNotContain { code, .. } => &code,
Self::Email { code, .. } => &code,
Self::Ip { code, .. } => &code,
Self::Length { code, .. } => &code,
Self::MustMatch { code, .. } => &code,
Self::Nested => "nested",
Self::NonControlCharacter { code, .. } => &code,
Self::Range { code, .. } => &code,
Self::Regex { code, .. } => &code,
Self::Required { code, .. } => &code,
Self::RequiredNested { code, .. } => &code,
Self::Url { code, .. } => &code,
}
}
}

#[derive(Debug, Clone, PartialEq, Serialize)]
#[serde(untagged)]
pub enum ValidationConstraintsKind {
Struct(Box<ValidationConstraints>),
Collection(Box<ValidationConstraints>),
Field(Vec<ValidationConstraint>),
}

#[derive(Default, Debug, Clone, PartialEq, Serialize)]
pub struct ValidationConstraints(pub HashMap<Cow<'static, str>, Vec<ValidationConstraintsKind>>);

impl ValidationConstraints {
pub fn add(&mut self, field: impl Into<Cow<'static, str>>, constraint: ValidationConstraint) {
let entry = self.0.entry(field.into()).or_default();

match entry.iter_mut().find_map(|kind| match kind {
ValidationConstraintsKind::Field(field) => Some(field),
_ => None,
}) {
Some(field) => field.push(constraint),
None => entry.push(ValidationConstraintsKind::Field(vec![constraint])),
}
}

pub fn merge(
&mut self,
field: impl Into<Cow<'static, str>>,
child: ValidationConstraints,
is_collection: bool,
) {
if is_collection {
self.add_nested(field.into(), ValidationConstraintsKind::Collection(Box::new(child)));
} else {
self.add_nested(field.into(), ValidationConstraintsKind::Struct(Box::new(child)));
}
}

fn add_nested(
&mut self,
field: impl Into<Cow<'static, str>>,
constraints: ValidationConstraintsKind,
) {
self.0.entry(field.into()).or_default().push(constraints);
}
}
Loading