Skip to content

Commit

Permalink
Better support for maps (#203)
Browse files Browse the repository at this point in the history
* generalized trait impl for HashMap and HashSet

* support for nested map validation

* properly quoting nested map field

* map nested validation tests
  • Loading branch information
krojew committed Apr 5, 2022
1 parent 3161a85 commit 7e85875
Show file tree
Hide file tree
Showing 3 changed files with 116 additions and 8 deletions.
8 changes: 4 additions & 4 deletions validator/src/traits.rs
Expand Up @@ -50,25 +50,25 @@ impl<'a, T> HasLen for &'a Vec<T> {
}
}

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

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

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

impl<T> HasLen for HashSet<T> {
impl<T, S> HasLen for HashSet<T, S> {
fn length(&self) -> u64 {
self.len() as u64
}
Expand Down
42 changes: 39 additions & 3 deletions validator_derive/src/quoting.rs
Expand Up @@ -44,7 +44,10 @@ impl FieldQuoter {
pub fn quote_validator_field(&self) -> proc_macro2::TokenStream {
let ident = &self.ident;

if self._type.starts_with("Option<") || self._type.starts_with("Vec<") {
if self._type.starts_with("Option<")
|| self._type.starts_with("Vec<")
|| is_map(&self._type)
{
quote!(#ident)
} else if COW_TYPE.is_match(self._type.as_ref()) {
quote!(self.#ident.as_ref())
Expand Down Expand Up @@ -89,7 +92,7 @@ impl FieldQuoter {

/// Wrap the quoted output of a validation with a for loop if
/// the field type is a vector
pub fn wrap_if_vector(&self, tokens: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
pub fn wrap_if_collection(&self, tokens: proc_macro2::TokenStream) -> proc_macro2::TokenStream {
let field_ident = &self.ident;
let field_name = &self.name;
if self._type.starts_with("Vec<") {
Expand All @@ -112,12 +115,45 @@ impl FieldQuoter {
}).collect();
result = ::validator::ValidationErrors::merge_all(result, #field_name, results);
});
} else if is_map(&self._type) {
if self._type.starts_with("Option<") {
return quote!(
if !::validator::ValidationErrors::has_error(&result, #field_name) {
let results: Vec<_> = #field_ident.iter().map(|(_, #field_ident)| {
let mut result = ::std::result::Result::Ok(());
#tokens
result
}).collect();
result = ::validator::ValidationErrors::merge_all(result, #field_name, results);
});
} else {
return quote!(
if !::validator::ValidationErrors::has_error(&result, #field_name) {
let results: Vec<_> = self.#field_ident.iter().map(|(_, #field_ident)| {
let mut result = ::std::result::Result::Ok(());
#tokens
result
}).collect();
result = ::validator::ValidationErrors::merge_all(result, #field_name, results);
});
}
}

tokens
}
}

fn is_map(_type: &str) -> bool {
if _type.starts_with("Option<") {
return is_map(&_type[7..]);
}

_type.starts_with("HashMap<")
|| _type.starts_with("FxHashMap<")
|| _type.starts_with("FnvHashMap<")
|| _type.starts_with("BTreeMap<")
}

/// Quote an actual end-user error creation automatically
fn quote_error(validation: &FieldValidation) -> proc_macro2::TokenStream {
let code = &validation.code;
Expand Down Expand Up @@ -462,7 +498,7 @@ pub fn quote_nested_validation(field_quoter: &FieldQuoter) -> proc_macro2::Token
let field_name = &field_quoter.name;
let validator_field = field_quoter.quote_validator_field();
let quoted = quote!(result = ::validator::ValidationErrors::merge(result, #field_name, #validator_field.validate()););
field_quoter.wrap_if_option(field_quoter.wrap_if_vector(quoted))
field_quoter.wrap_if_option(field_quoter.wrap_if_collection(quoted))
}

pub fn quote_validator(
Expand Down
74 changes: 73 additions & 1 deletion validator_derive_tests/tests/nested.rs
Expand Up @@ -48,7 +48,21 @@ struct ParentWithOptionVectorOfChildren {
child: Option<Vec<Child>>,
}

#[derive(Debug, Validate, Serialize)]
#[derive(Debug, Validate)]
struct ParentWithMapOfChildren {
#[validate]
#[validate(length(min = 1))]
child: HashMap<i8, Child>,
}

#[derive(Debug, Validate)]
struct ParentWithOptionMapOfChildren {
#[validate]
#[validate(length(min = 1))]
child: Option<HashMap<i8, Child>>,
}

#[derive(Debug, Validate, Serialize, Clone)]
struct Child {
#[validate(length(min = 1))]
value: String,
Expand Down Expand Up @@ -273,6 +287,64 @@ fn test_can_validate_option_vector_fields() {
}
}

#[test]
fn test_can_validate_map_fields() {
let instance = ParentWithMapOfChildren {
child: [(0, Child { value: String::new() })].iter().cloned().collect(),
};

let res = instance.validate();
assert!(res.is_err());
let err = res.unwrap_err();
let errs = err.errors();
assert_eq!(errs.len(), 1);
assert!(errs.contains_key("child"));
if let ValidationErrorsKind::List(ref errs) = errs["child"] {
assert!(errs.contains_key(&0));
unwrap_map(&errs[&0], |errs| {
assert_eq!(errs.len(), 1);
assert!(errs.contains_key("value"));
if let ValidationErrorsKind::Field(ref errs) = errs["value"] {
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].code, "length");
} else {
panic!("Expected field validation errors");
}
});
} else {
panic!("Expected list validation errors");
}
}

#[test]
fn test_can_validate_option_map_fields() {
let instance = ParentWithOptionMapOfChildren {
child: Some([(0, Child { value: String::new() })].iter().cloned().collect()),
};

let res = instance.validate();
assert!(res.is_err());
let err = res.unwrap_err();
let errs = err.errors();
assert_eq!(errs.len(), 1);
assert!(errs.contains_key("child"));
if let ValidationErrorsKind::List(ref errs) = errs["child"] {
assert!(errs.contains_key(&0));
unwrap_map(&errs[&0], |errs| {
assert_eq!(errs.len(), 1);
assert!(errs.contains_key("value"));
if let ValidationErrorsKind::Field(ref errs) = errs["value"] {
assert_eq!(errs.len(), 1);
assert_eq!(errs[0].code, "length");
} else {
panic!("Expected field validation errors");
}
});
} else {
panic!("Expected list validation errors");
}
}

#[test]
fn test_field_validations_take_priority_over_nested_validations() {
let instance = ParentWithVectorOfChildren { child: Vec::new() };
Expand Down

0 comments on commit 7e85875

Please sign in to comment.