diff options
| -rw-r--r-- | README.md | 81 | ||||
| -rw-r--r-- | validator/src/lib.rs | 2 | ||||
| -rw-r--r-- | validator/src/types.rs | 98 | ||||
| -rw-r--r-- | validator/src/validation/mod.rs | 2 | ||||
| -rw-r--r-- | validator_derive/src/lib.rs | 17 | ||||
| -rw-r--r-- | validator_derive/src/quoting.rs | 70 | ||||
| -rw-r--r-- | validator_derive/tests/compile-fail/no_nested_validations.rs | 17 | ||||
| -rw-r--r-- | validator_derive/tests/complex.rs | 129 | ||||
| -rw-r--r-- | validator_derive/tests/contains.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/credit_card.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/custom.rs | 4 | ||||
| -rw-r--r-- | validator_derive/tests/email.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/length.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/must_match.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/nested.rs | 370 | ||||
| -rw-r--r-- | validator_derive/tests/phone.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/range.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/regex.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/schema.rs | 6 | ||||
| -rw-r--r-- | validator_derive/tests/url.rs | 6 |
20 files changed, 783 insertions, 67 deletions
@@ -53,7 +53,22 @@ match signup_data.validate() { }; ``` -An error has the following structure: +The `validate()` method returns a `Result<(), ValidationErrors>`. In the case of an invalid result, the +`ValidationErrors` instance includes a map of errors keyed against the struct's field names. Errors may be represented +in three ways, as described by the `ValidationErrorsKind` enum: + +```rust +#[derive(Debug, Serialize, Clone, PartialEq)] +#[serde(untagged)] +pub enum ValidationErrorsKind { + Struct(Box<ValidationErrors>), + List(BTreeMap<usize, Box<ValidationErrors>>), + Field(Vec<ValidationError>), +} +``` + +In the simple example above, any errors would be of the `Field(Vec<ValidationError>)` type, where a single +`ValidationError` has the following structure: ```rust #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] @@ -73,6 +88,57 @@ If you are adding a validation on a `Option<..>` field, it will only be ran if t being `must_match` that doesn't currently work with `Option` due to me not finding a use case for it. If you have one, please comment on https://github.com/Keats/validator/issues/7. +The other two `ValidationErrorsKind` types represent errors discovered in nested (vectors of) structs, as described in +this example: + + ```rust +#[macro_use] +extern crate validator_derive; +extern crate validator; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; + +#[derive(Debug, Validate, Deserialize)] +struct SignupData { + #[validate] + contact_details: ContactDetails, + #[validate] + preferences: Vec<Preference> +} + +#[derive(Debug, Validate, Deserialize)] +struct ContactDetails { + #[validate(email)] + mail: String, + #[validate(phone)] + phone: String +} + +#[derive(Debug, Validate, Deserialize)] +struct Preference { + #[validate(length(min = "4"))] + name: String, + value: bool, +} + +match signup_data.validate() { + Ok(_) => (), + Err(e) => return e; +}; + ``` + +Here, the `ContactDetails` and `Preference` structs are nested within the parent `SignupData` struct. Because +these child types also derive `Validate`, the fields where they appear can be tagged for inclusion in the parent +struct's validation method. + +Any errors found in a single nested struct (the `contact_details` field in this example) would be returned as a +`Struct(Box<ValidationErrors>)` type in the parent's `ValidationErrors` result. + +Any errors found in a vector of nested structs (the `preferences` field in this example) would be returned as a +`List(BTreeMap<usize, Box<ValidationErrors>>)` type in the parent's `ValidationErrors` result, where the map is keyed on +the index of invalid vector entries. + ## Usage You will need to import the `Validate` trait, and optionally use the `attr_literals` feature. @@ -144,7 +210,7 @@ Examples: ``` ### regex -Tests whether the string matchs the regex given. `regex` takes +Tests whether the string matches the regex given. `regex` takes 1 string argument: the path to a static Regex instance. Examples: @@ -154,7 +220,7 @@ Examples: ``` ### credit\_card -Test whetever the string is a valid credit card number. +Test whether the string is a valid credit card number. Examples: @@ -181,6 +247,15 @@ Examples: #[validate(custom = "::utils::validate_something")] ``` +### nested +Performs validation on a field with a type that also implements the Validate trait (or a vector of such types). + +Examples: + +```rust +#[validate] +``` + ## Struct level validation Often, some error validation can only be applied when looking at the full struct, here's how it works here: diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 6cb0cb1..ef9685f 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -29,5 +29,5 @@ pub use validation::cards::validate_credit_card; pub use validation::phone::validate_phone; pub use validation::Validator; -pub use types::{ValidationErrors, ValidationError}; +pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind}; pub use traits::{Validate, HasLen, Contains}; diff --git a/validator/src/types.rs b/validator/src/types.rs index 5c62e6d..3981989 100644 --- a/validator/src/types.rs +++ b/validator/src/types.rs @@ -1,6 +1,6 @@ use std::{self, fmt}; use std::borrow::Cow; -use std::collections::HashMap; +use std::collections::{BTreeMap, HashMap, hash_map::Entry::Vacant}; use serde_json::{Value, to_value}; use serde::ser::Serialize; @@ -8,9 +8,9 @@ use serde::ser::Serialize; #[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] pub struct ValidationError { - pub code: Cow<'static, str>, - pub message: Option<Cow<'static, str>>, - pub params: HashMap<Cow<'static, str>, Value>, + pub code: Cow<'static, str>, + pub message: Option<Cow<'static, str>>, + pub params: HashMap<Cow<'static, str>, Value>, } impl ValidationError { @@ -39,25 +39,109 @@ impl std::error::Error for ValidationError { } #[derive(Debug, Serialize, Clone, PartialEq)] -pub struct ValidationErrors(HashMap<&'static str, Vec<ValidationError>>); +#[serde(untagged)] +pub enum ValidationErrorsKind { + Struct(Box<ValidationErrors>), + List(BTreeMap<usize, Box<ValidationErrors>>), + Field(Vec<ValidationError>), +} +#[derive(Debug, Serialize, Clone, PartialEq)] +pub struct ValidationErrors(HashMap<&'static str, ValidationErrorsKind>); impl ValidationErrors { pub fn new() -> ValidationErrors { ValidationErrors(HashMap::new()) } - pub fn inner(self) -> HashMap<&'static str, Vec<ValidationError>> { + /// Returns a boolean indicating whether a validation result includes validation errors for a + /// given field. May be used as a condition for performing nested struct validations on a field + /// in the absence of field-level validation errors. + pub fn has_error(result: &Result<(), ValidationErrors>, field: &'static str) -> bool { + match result { + Ok(()) => false, + Err(ref errs) => errs.contains_key(field), + } + } + + /// Returns the combined outcome of a struct's validation result along with the nested + /// validation result for one of its fields. + pub fn merge(parent: Result<(), ValidationErrors>, field: &'static str, child: Result<(), ValidationErrors>) -> Result<(), ValidationErrors> { + match child { + Ok(()) => parent, + Err(errors) => parent.and_then(|_| Err(ValidationErrors::new())).map_err(|mut parent_errors| { + parent_errors.add_nested(field, ValidationErrorsKind::Struct(Box::new(errors))); + parent_errors + }) + } + } + + /// Returns the combined outcome of a struct's validation result along with the nested + /// validation result for one of its fields where that field is a vector of validating structs. + pub fn merge_all(parent: Result<(), ValidationErrors>, field: &'static str, children: Vec<Result<(), ValidationErrors>>) -> Result<(), ValidationErrors> { + let errors = children.into_iter().enumerate() + .filter_map(|(i, res)| res.err().map(|mut err| (i, err.remove(field)))) + .filter_map(|(i, entry)| match entry { + Some(ValidationErrorsKind::Struct(errors)) => Some((i, errors)), + _ => None, + }) + .collect::<BTreeMap<_, _>>(); + + if errors.is_empty() { + parent + } else { + parent.and_then(|_| Err(ValidationErrors::new())).map_err(|mut parent_errors| { + parent_errors.add_nested(field, ValidationErrorsKind::List(errors)); + parent_errors + }) + } + } + + /// Returns a map of field-level validation errors found for the struct that was validated and + /// any of it's nested structs that are tagged for validation. + pub fn errors(self) -> HashMap<&'static str, ValidationErrorsKind> { self.0 } + /// Returns a map of only field-level validation errors found for the struct that was validated. + pub fn field_errors(self) -> HashMap<&'static str, Vec<ValidationError>> { + self.0.into_iter() + .filter_map(|(k, v)| if let ValidationErrorsKind::Field(errors) = v { Some((k, errors)) } else { None }) + .collect() + } + + #[deprecated(since="0.7.3", note="Use `field_errors` instead, or `errors` to also access any errors from nested structs")] + pub fn inner(self) -> HashMap<&'static str, Vec<ValidationError>> { + self.field_errors() + } + pub fn add(&mut self, field: &'static str, error: ValidationError) { - self.0.entry(field).or_insert_with(|| vec![]).push(error); + if let ValidationErrorsKind::Field(ref mut vec) = self.0.entry(field).or_insert(ValidationErrorsKind::Field(vec![])) { + vec.push(error); + } else { + panic!("Attempt to add field validation to a non-Field ValidationErrorsKind instance"); + } } pub fn is_empty(&self) -> bool { self.0.is_empty() } + + fn add_nested(&mut self, field: &'static str, errors: ValidationErrorsKind) { + if let Vacant(entry) = self.0.entry(field) { + entry.insert(errors); + } else { + panic!("Attempt to replace non-empty ValidationErrors entry"); + } + } + + fn contains_key(&self, field: &'static str) -> bool { + self.0.contains_key(field) + } + + fn remove(&mut self, field: &'static str) -> Option<ValidationErrorsKind> { + self.0.remove(field) + } } impl std::error::Error for ValidationErrors { diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs index 3f1e158..34d1087 100644 --- a/validator/src/validation/mod.rs +++ b/validator/src/validation/mod.rs @@ -40,6 +40,7 @@ pub enum Validator { CreditCard, #[cfg(feature = "phone")] Phone, + Nested, } impl Validator { @@ -57,6 +58,7 @@ impl Validator { Validator::CreditCard => "credit_card", #[cfg(feature = "phone")] Validator::Phone => "phone", + Validator::Nested => "nested", } } } diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index b5a76c1..2a23ba4 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -33,9 +33,7 @@ use quoting::{FieldQuoter, quote_field_validation, quote_schema_validation}; #[proc_macro_derive(Validate, attributes(validate))] pub fn derive_validation(input: TokenStream) -> TokenStream { let ast = syn::parse(input).unwrap(); - - let expanded = impl_validate(&ast); - expanded.into() + impl_validate(&ast).into() } @@ -52,6 +50,7 @@ fn impl_validate(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { }; let mut validations = vec![]; + let mut nested_validations = vec![]; let field_types = find_fields_type(&fields); @@ -62,7 +61,7 @@ fn impl_validate(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { let field_quoter = FieldQuoter::new(field_ident, name, field_type); for validation in &field_validations { - validations.push(quote_field_validation(&field_quoter, validation)); + quote_field_validation(&field_quoter, validation, &mut validations, &mut nested_validations); } } @@ -82,11 +81,14 @@ fn impl_validate(ast: &syn::DeriveInput) -> proc_macro2::TokenStream { #schema_validation - if errors.is_empty() { + let mut result = if errors.is_empty() { ::std::result::Result::Ok(()) } else { ::std::result::Result::Err(errors) - } + }; + + #(#nested_validations)* + result } } ); @@ -352,6 +354,9 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S }; } }, + Some(syn::Meta::Word(_)) => { + validators.push(FieldValidation::new(Validator::Nested)) + }, _ => unreachable!("Got something other than a list of attributes while checking field `{}`", field_ident), } } diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs index 20ca7a7..b47db6a 100644 --- a/validator_derive/src/quoting.rs +++ b/validator_derive/src/quoting.rs @@ -40,13 +40,25 @@ 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<") { + quote!(#ident) + } else if COW_TYPE.is_match(&self._type.as_ref()) { + quote!(self.#ident.as_ref()) + } else { + quote!(self.#ident) + } + } + pub fn get_optional_validator_param(&self) -> proc_macro2::TokenStream { let ident = &self.ident; if self._type.starts_with("Option<&") || self._type.starts_with("Option<Option<&") || NUMBER_TYPES.contains(&self._type.as_ref()) { - quote!(#ident) + quote!(#ident) } else { - quote!(ref #ident) + quote!(ref #ident) } } @@ -71,6 +83,27 @@ impl FieldQuoter { tokens } + + + /// 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 { + let field_ident = &self.ident; + let field_name = &self.name; + if self._type.starts_with("Vec<") { + 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 + } } /// Quote an actual end-user error creation automatically @@ -326,24 +359,33 @@ pub fn quote_regex_validation(field_quoter: &FieldQuoter, validation: &FieldVali unreachable!(); } -pub fn quote_field_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> proc_macro2::TokenStream { +pub fn quote_nested_validation(field_quoter: &FieldQuoter) -> proc_macro2::TokenStream { + 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)) +} + +pub fn quote_field_validation(field_quoter: &FieldQuoter, validation: &FieldValidation, + validations: &mut Vec<proc_macro2::TokenStream>, + nested_validations: &mut Vec<proc_macro2::TokenStream>) { match validation.validator { - Validator::Length {..} => quote_length_validation(&field_quoter, validation), - Validator::Range {..} => quote_range_validation(&field_quoter, validation), - Validator::Email => quote_email_validation(&field_quoter, validation), - Validator::Url => quote_url_validation(&field_quoter, validation), - Validator::MustMatch(_) => quote_must_match_validation(&field_quoter, validation), - Validator::Custom(_) => quote_custom_validation(&field_quoter, validation), - Validator::Contains(_) => quote_contains_validation(&field_quoter, validation), - Validator::Regex(_) => quote_regex_validation(&field_quoter, validation), + Validator::Length {..} => validations.push(quote_length_validation(&field_quoter, validation)), + Validator::Range {..} => validations.push(quote_range_validation(&field_quoter, validation)), + Validator::Email => validations.push(quote_email_validation(&field_quoter, validation)), + Validator::Url => validations.push(quote_url_validation(&field_quoter, validation)), + Validator::MustMatch(_) => validations.push(quote_must_match_validation(&field_quoter, validation)), + Validator::Custom(_) => validations.push(quote_custom_validation(&field_quoter, validation)), + Validator::Contains(_) => validations.push(quote_contains_validation(&field_quoter, validation)), + Validator::Regex(_) => validations.push(quote_regex_validation(&field_quoter, validation)), #[cfg(feature = "card")] - Validator::CreditCard => quote_credit_card_validation(&field_quoter, validation), + Validator::CreditCard => validations.push(quote_credit_card_validation(&field_quoter, validation)), #[cfg(feature = "phone")] - Validator::Phone => quote_phone_validation(&field_quoter, validation), + Validator::Phone => validations.push(quote_phone_validation(&field_quoter, validation)), + Validator::Nested => nested_validations.push(quote_nested_validation(&field_quoter)), } } - pub fn quote_schema_validation(validation: Option<SchemaValidation>) -> proc_macro2::TokenStream { if let Some(v) = validation { let fn_ident = syn::Ident::new(&v.function, Span::call_site()); diff --git a/validator_derive/tests/compile-fail/no_nested_validations.rs b/validator_derive/tests/compile-fail/no_nested_validations.rs new file mode 100644 index 0000000..788152d --- /dev/null +++ b/validator_derive/tests/compile-fail/no_nested_validations.rs @@ -0,0 +1,17 @@ +#[macro_use] extern crate validator_derive; +extern crate validator; +use validator::Validate; + +#[derive(Validate)] +//~^ ERROR: no method named `validate` found for type `Nested` in the current scope [E0599] +//~^^ HELP: items from traits can only be used if the trait is implemented and in scope +struct Test { + #[validate] + nested: Nested, +} + +struct Nested { + value: String +} + +fn main() {} diff --git a/validator_derive/tests/complex.rs b/validator_derive/tests/complex.rs index bbbc2e6..0df26c0 100644 --- a/validator_derive/tests/complex.rs +++ b/validator_derive/tests/complex.rs @@ -9,7 +9,8 @@ extern crate regex; extern crate lazy_static; use regex::Regex; -use validator::{Validate, ValidationError, ValidationErrors}; +use validator::{Validate, ValidationError, ValidationErrors, ValidationErrorsKind}; +use std::collections::HashMap; fn validate_unique_username(username: &str) -> Result<(), ValidationError> { @@ -40,8 +41,34 @@ struct SignupData { first_name: String, #[validate(range(min = "18", max = "20"))] age: u32, + #[validate] + phone: Phone, + #[validate] + card: Option<Card>, + #[validate] + preferences: Vec<Preference> } +#[derive(Debug, Validate, Deserialize)] +struct Phone { + #[validate(phone)] + number: String +} + +#[derive(Debug, Validate, Deserialize)] +struct Card { + #[validate(credit_card)] + number: String, + #[validate(range(min = "100", max = "9999"))] + cvv: u32, +} + +#[derive(Debug, Validate, Deserialize)] +struct Preference { + #[validate(length(min = "4"))] + name: String, + value: bool, +} #[test] fn is_fine_with_many_valid_validations() { @@ -50,6 +77,19 @@ fn is_fine_with_many_valid_validations() { site: "http://hello.com".to_string(), first_name: "Bob".to_string(), age: 18, + phone: Phone { + number: "+14152370800".to_string() + }, + card: Some(Card { + number: "5236313877109142".to_string(), + cvv: 123 + }), + preferences: vec![ + Preference { + name: "marketing".to_string(), + value: false + }, + ] }; assert!(signup.validate().is_ok()); @@ -62,13 +102,82 @@ fn failed_validation_points_to_original_field_name() { site: "http://hello.com".to_string(), first_name: "".to_string(), age: 18, + phone: Phone { + number: "123 invalid".to_string(), + }, + card: Some(Card { + number: "1234567890123456".to_string(), + cvv: 1 + }), + preferences: vec![ + Preference { + name: "abc".to_string(), + value: true + }, + ] }; let res = signup.validate(); + // println!("{}", serde_json::to_string(&res).unwrap()); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().errors(); assert!(errs.contains_key("firstName")); - assert_eq!(errs["firstName"].len(), 1); - assert_eq!(errs["firstName"][0].code, "length"); + if let ValidationErrorsKind::Field(ref err) = errs["firstName"] { + assert_eq!(err.len(), 1); + assert_eq!(err[0].code, "length"); + } else { + panic!("Expected field validation errors"); + } + assert!(errs.contains_key("phone")); + if let ValidationErrorsKind::Struct(ref errs) = errs["phone"] { + unwrap_map(errs, |errs| { + assert_eq!(errs.len(), 1); + assert!(errs.contains_key("number")); + if let ValidationErrorsKind::Field(ref errs) = errs["number"] { + assert_eq!(errs.len(), 1); + assert_eq!(errs[0].code, "phone"); + } else { + panic!("Expected field validation errors"); + } + }); + } else { + panic!("Expected struct validation errors"); + } + assert!(errs.contains_key("card")); + if let ValidationErrorsKind::Struct(ref errs) = errs["card"] { + unwrap_map(errs, |errs| { + assert_eq!(errs.len(), 2); + assert!(errs.contains_key("number")); + if let ValidationErrorsKind::Field(ref err) = errs["number"] { + assert_eq!(err.len(), 1); + assert_eq!(err[0].code, "credit_card"); + } else { + panic!("Expected field validation errors"); + } + assert!(errs.contains_key("cvv")); + if let ValidationErrorsKind::Field(ref err) = errs["cvv"] { + assert_eq!(err.len(), 1); + assert_eq!(err[0].code, "range"); + } else { + panic!("Expected field validation errors"); + } + }); + } else { + panic!("Expected struct validation errors"); + } + assert!(errs.contains_key("preferences")); + if let ValidationErrorsKind::List(ref errs) = errs["preferences"] { + assert!(errs.contains_key(&0)); + unwrap_map(&errs[&0], |errs| { + assert_eq!(errs.len(), 1); + assert!(errs.contains_key("name")); + if let ValidationErrorsKind::Field(ref err) = errs["name"] { + assert_eq!(err.len(), 1); + assert_eq!(err[0].code, "length"); + } + }); + } else { + panic!("Expected list validation errors"); + } } #[test] @@ -177,6 +286,11 @@ fn test_works_with_question_mark_operator() { site: "http://hello.com".to_string(), first_name: "Bob".to_string(), age: 18, + phone: Phone { + number: "+14152370800".to_string() + }, + card: None, + preferences: Vec::new(), }; signup.validate()?; @@ -216,4 +330,11 @@ fn test_works_with_none_values() { assert!(p.validate().is_ok()); assert!(q.validate().is_ok()); +} + +fn unwrap_map<F>(errors: &Box<ValidationErrors>, f: F) + where F: FnOnce(HashMap<&'static str, ValidationErrorsKind>) +{ + let errors = *errors.clone(); + f(errors.errors()); }
\ No newline at end of file diff --git a/validator_derive/tests/contains.rs b/validator_derive/tests/contains.rs index 847de0f..5e9060e 100644 --- a/validator_derive/tests/contains.rs +++ b/validator_derive/tests/contains.rs @@ -32,7 +32,7 @@ fn value_not_containing_needle_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "contains"); @@ -52,7 +52,7 @@ fn can_specify_code_for_contains() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -70,7 +70,7 @@ fn can_specify_message_for_contains() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/credit_card.rs b/validator_derive/tests/credit_card.rs index a430971..b1a9da3 100644 --- a/validator_derive/tests/credit_card.rs +++ b/validator_derive/tests/credit_card.rs @@ -34,7 +34,7 @@ fn bad_credit_card_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "credit_card"); @@ -54,7 +54,7 @@ fn can_specify_code_for_credit_card() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -73,7 +73,7 @@ fn can_specify_message_for_credit_card() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/custom.rs b/validator_derive/tests/custom.rs index 0cac75f..cd1b354 100644 --- a/validator_derive/tests/custom.rs +++ b/validator_derive/tests/custom.rs @@ -40,7 +40,7 @@ fn can_fail_custom_fn_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "meh"); @@ -59,7 +59,7 @@ fn can_specify_message_for_custom_fn() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/email.rs b/validator_derive/tests/email.rs index fc05bf9..4fbc123 100644 --- a/validator_derive/tests/email.rs +++ b/validator_derive/tests/email.rs @@ -33,7 +33,7 @@ fn bad_email_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "email"); @@ -52,7 +52,7 @@ fn can_specify_code_for_email() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -70,7 +70,7 @@ fn can_specify_message_for_email() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/length.rs b/validator_derive/tests/length.rs index 892cf74..12bffc6 100644 --- a/validator_derive/tests/length.rs +++ b/validator_derive/tests/length.rs @@ -32,7 +32,7 @@ fn value_out_of_length_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "length"); @@ -53,7 +53,7 @@ fn can_specify_code_for_length() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -71,7 +71,7 @@ fn can_specify_message_for_length() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/must_match.rs b/validator_derive/tests/must_match.rs index 038616a..a8c95f9 100644 --- a/validator_derive/tests/must_match.rs +++ b/validator_derive/tests/must_match.rs @@ -38,7 +38,7 @@ fn not_matching_fails_validation() { let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "must_match"); @@ -60,7 +60,7 @@ fn can_specify_code_for_must_match() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -80,7 +80,7 @@ fn can_specify_message_for_must_match() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/nested.rs b/validator_derive/tests/nested.rs new file mode 100644 index 0000000..4f03714 --- /dev/null +++ b/validator_derive/tests/nested.rs @@ -0,0 +1,370 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; +#[macro_use] +extern crate serde_derive; + +use validator::{validate_length, Validate, ValidationError, ValidationErrors, ValidationErrorsKind, Validator}; +use std::{borrow::Cow, collections::HashMap}; + +#[derive(Debug, Validate)] +struct Root<'a> { + #[validate(length(min = "1"))] + value: String, + + #[validate] + a: &'a A, +} + +#[derive(Debug, Validate)] +struct A { + #[validate(length(min = "1"))] + value: String, + + #[validate] + b: B, +} + +#[derive(Debug, Validate)] +struct B { + #[validate(length(min = "1"))] + value: String, +} + +#[derive(Debug, Validate)] +struct ParentWithOptionalChild { + #[validate] + child: Option<Child>, +} + +#[derive(Debug, Validate)] +struct ParentWithVectorOfChildren { + #[validate] + #[validate(length(min = "1"))] + child: Vec<Child>, +} + +#[derive(Debug, Validate, Serialize)] +struct Child { + #[validate(length(min = "1"))] + value: String, +} + +#[test] +fn is_fine_with_nested_validations() { + let root = Root { + value: "valid".to_string(), + a: &A { + value: "valid".to_string(), + b: B { + value: "valid".to_string(), + } + } + }; + + assert!(root.validate().is_ok()); +} + +#[test] +fn failed_validation_points_to_original_field_names() { + let root = Root { + value: String::new(), + a: &A { + value: String::new(), + b: B { + value: String::new(), + } + } + }; + + let res = root.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().errors(); + assert_eq!(errs.len(), 2); + 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"); + } + assert!(errs.contains_key("a")); + if let ValidationErrorsKind::Struct(ref errs) = errs["a"] { + unwrap_map(errs, |errs| { + assert_eq!(errs.len(), 2); + 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"); + } + assert!(errs.contains_key("b")); + if let ValidationErrorsKind::Struct(ref errs) = errs["b"] { + unwrap_map(errs, |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 struct validation errors"); + } + }); + } else { + panic!("Expected struct validation errors"); + } +} + +#[test] +fn test_can_validate_option_fields_without_lifetime() { + let instance = ParentWithOptionalChild { + child: Some(Child { + value: String::new(), + }) + }; + + let res = instance.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().errors(); + assert_eq!(errs.len(), 1); + assert!(errs.contains_key("child")); + if let ValidationErrorsKind::Struct(ref errs) = errs["child"] { + unwrap_map(errs, |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 struct validation errors"); + } +} + +#[test] +fn test_can_validate_option_fields_with_lifetime() { + #[derive(Debug, Validate)] + struct ParentWithLifetimeAndOptionalChild<'a> { + #[validate] + child: Option<&'a Child>, + } + + let child = Child { + value: String::new(), + }; + + let instance = ParentWithLifetimeAndOptionalChild { + child: Some(&child) + }; + + let res = instance.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().errors(); + assert_eq!(errs.len(), 1); + assert!(errs.contains_key("child")); + if let ValidationErrorsKind::Struct(ref errs) = errs["child"] { + unwrap_map(errs, |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 struct validation errors"); + } +} + +#[test] +fn test_works_with_none_values() { + let instance = ParentWithOptionalChild { + child: None, + }; + + let res = instance.validate(); + assert!(res.is_ok()); +} + +#[test] +fn test_can_validate_vector_fields() { + let instance = ParentWithVectorOfChildren { + child: vec![ + Child { + value: "valid".to_string(), + }, + Child { + value: String::new(), + }, + Child { + value: "valid".to_string(), + }, + Child { + value: String::new(), + } + ], + }; + + let res = instance.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().errors(); + assert_eq!(errs.len(), 1); + assert!(errs.contains_key("child")); + if let ValidationErrorsKind::List(ref errs) = errs["child"] { + assert!(errs.contains_key(&1)); + unwrap_map(&errs[&1], |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"); + } + }); + assert!(errs.contains_key(&3)); + unwrap_map(&errs[&3], |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(), + }; + + let res = instance.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().errors(); + assert_eq!(errs.len(), 1); + assert!(errs.contains_key("child")); + if let ValidationErrorsKind::Field(ref errs) = errs["child"] { + assert_eq!(errs.len(), 1); + assert_eq!(errs[0].code, "length"); + } else { + panic!("Expected field validation errors"); + } +} + +#[test] +#[should_panic(expected = "Attempt to replace non-empty ValidationErrors entry")] +#[allow(unused)] +fn test_field_validation_errors_replaced_with_nested_validations_fails() { + + #[derive(Debug)] + struct ParentWithOverridingStructValidations { + child: Vec<Child>, + } + + impl Validate for ParentWithOverridingStructValidations { + // Evaluating structs after fields validations have discovered errors should fail because + // field validations are expected to take priority over nested struct validations + #[allow(unused_mut)] + fn validate(&self) -> Result<(), ValidationErrors> { + // First validate the length of the vector: + let mut errors = ValidationErrors::new(); + if !validate_length(Validator::Length { min: Some(2u64), max: None, equal: None }, &self.child) { + let mut err = ValidationError::new("length"); + err.add_param(Cow::from("min"), &2u64); + err.add_param(Cow::from("value"), &&self.child); + errors.add("child", err); + } + + // Then validate the nested vector of structs without checking for existing field errors: + let mut result = if errors.is_empty() { Ok(()) } else { Err(errors) }; + { + let results: Vec<_> = self.child.iter().map(|child| { + let mut result = Ok(()); + result = ValidationErrors::merge(result, "child", child.validate()); + result + }).collect(); + result = ValidationErrors::merge_all(result, "child", results); + } + result + } + } + + let instance = ParentWithOverridingStructValidations { + child: vec![ + Child { + value: String::new() + }] + }; + instance.validate(); +} + +#[test] +#[should_panic(expected = "Attempt to add field validation to a non-Field ValidationErrorsKind instance")] +#[allow(unused)] +fn test_field_validations_evaluated_after_nested_validations_fails() { + #[derive(Debug)] + struct ParentWithStructValidationsFirst { + child: Vec<Child>, + } + + impl Validate for ParentWithStructValidationsFirst { + // Evaluating fields after their nested structs should fail because field + // validations are expected to take priority over nested struct validations + #[allow(unused_mut)] + fn validate(&self) -> Result<(), ValidationErrors> { + // First validate the nested vector of structs: + let mut result = Ok(()); + if !ValidationErrors::has_error(&result, "child") { + let results: Vec<_> = self.child.iter().map(|child| { + let mut result = Ok(()); + result = ValidationErrors::merge(result, "child", child.validate()); + result + }).collect(); + result = ValidationErrors::merge_all(result, "child", results); + } + + // Then validate the length of the vector itself: + if !validate_length(Validator::Length { min: Some(2u64), max: None, equal: None }, &self.child) { + let mut err = ValidationError::new("length"); + err.add_param(Cow::from("min"), &2u64); + err.add_param(Cow::from("value"), &&self.child); + result = result.and_then(|_| Err(ValidationErrors::new())).map_err(|mut errors| { + errors.add("child", err); + errors + }); + } + result + } + } + + let instance = ParentWithStructValidationsFirst { + child: vec![ + Child { + value: String::new() + }] + }; + let res = instance.validate(); +} + +fn unwrap_map<F>(errors: &Box<ValidationErrors>, f: F) + where F: FnOnce(HashMap<&'static str, ValidationErrorsKind>) +{ + let errors = *errors.clone(); + f(errors.errors()); +}
\ No newline at end of file diff --git a/validator_derive/tests/phone.rs b/validator_derive/tests/phone.rs index ad8f3cd..3131bad 100644 --- a/validator_derive/tests/phone.rs +++ b/validator_derive/tests/phone.rs @@ -35,7 +35,7 @@ fn bad_phone_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "phone"); @@ -54,7 +54,7 @@ fn can_specify_code_for_phone() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -74,7 +74,7 @@ fn can_specify_message_for_phone() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/range.rs b/validator_derive/tests/range.rs index 2ecd678..084e530 100644 --- a/validator_derive/tests/range.rs +++ b/validator_derive/tests/range.rs @@ -32,7 +32,7 @@ fn value_out_of_range_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "range"); @@ -50,7 +50,7 @@ fn can_specify_code_for_range() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -71,7 +71,7 @@ fn can_specify_message_for_range() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/regex.rs b/validator_derive/tests/regex.rs index 58f8f69..b54df03 100644 --- a/validator_derive/tests/regex.rs +++ b/validator_derive/tests/regex.rs @@ -40,7 +40,7 @@ fn bad_value_for_regex_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "regex"); @@ -59,7 +59,7 @@ fn can_specify_code_for_regex() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -77,7 +77,7 @@ fn can_specify_message_for_regex() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); diff --git a/validator_derive/tests/schema.rs b/validator_derive/tests/schema.rs index 6b3ae4f..32684fa 100644 --- a/validator_derive/tests/schema.rs +++ b/validator_derive/tests/schema.rs @@ -41,7 +41,7 @@ fn can_fail_schema_fn_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("__all__")); assert_eq!(errs["__all__"].len(), 1); assert_eq!(errs["__all__"][0].code, "meh"); @@ -63,7 +63,7 @@ fn can_specify_message_for_schema_fn() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("__all__")); assert_eq!(errs["__all__"].len(), 1); assert_eq!(errs["__all__"][0].clone().message.unwrap(), "oops"); @@ -89,7 +89,7 @@ fn can_choose_to_run_schema_validation_even_after_field_errors() { let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("__all__")); assert_eq!(errs["__all__"].len(), 1); assert_eq!(errs["__all__"][0].clone().code, "meh"); diff --git a/validator_derive/tests/url.rs b/validator_derive/tests/url.rs index 1182086..5a3b791 100644 --- a/validator_derive/tests/url.rs +++ b/validator_derive/tests/url.rs @@ -33,7 +33,7 @@ fn bad_url_fails_validation() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "url"); @@ -51,7 +51,7 @@ fn can_specify_code_for_url() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].code, "oops"); @@ -70,7 +70,7 @@ fn can_specify_message_for_url() { }; let res = s.validate(); assert!(res.is_err()); - let errs = res.unwrap_err().inner(); + let errs = res.unwrap_err().field_errors(); assert!(errs.contains_key("val")); assert_eq!(errs["val"].len(), 1); assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); |
