diff options
| author | Simon Sparks | 2018-09-13 17:33:49 +0100 |
|---|---|---|
| committer | Vincent Prouillet | 2018-09-13 18:33:49 +0200 |
| commit | dfdc289626c448522c43c13f8d72033fe0d1cae8 (patch) | |
| tree | c2fcd006466c587040be8b88076c1477d908effc /validator_derive/src | |
| parent | 5edb34ac261f7025ee0bb9c25ec1c216ded5aba2 (diff) | |
| download | validator-dfdc289626c448522c43c13f8d72033fe0d1cae8.tar.bz2 | |
Nested Validation (#60)
* Nested Validation
Added support for a plain validate attribute on a struct's field to
imply that validate() should be called on it with results being merged.
Validation errors are now keyed with a string that indicates the path to
the invalid field within a complex data structure. The errors themselves
also include the path to the invalid field, expressed as a vector of
strings.
Added support for option and vector fields where the wrapped values
perform nested validation.
Refactored vector wrapping for more reusable nested validation quoting.
Vector index is now more conveniently represented as an individual item
in the ValidationError's path attribute.
A few ergonomic changes to support custom (i.e. non-derived) Validate
implementations. A custom Validator implementation may either implement
the validate() method as before or implement the new
validate_path(ValidationPath) method which gives context of where the
validation is taking place in a complex data structure.
It is not necessary to implement both methods.
Refactored ValidationErrors to adopt a structure reflecting that of the
data being validated.
Instead of holding a vector of ValidationError instances for each field,
the ValidationErrors map may now include 3 different kinds of error
values representing the field, nested struct and nested vector of struct
scenarios.
Note that this implies a breaking change to the ValidationErrors map and
the "inner" method signature for accessing errors programmatically
compared to previous versions.
Added new accessor methods to the ValidationErrors type for retrieving
either field-level errors for a validated struct or all errors for the
struct and it's nested children.
The existing `inner` method provides the field-level behaviour for
backwards compatibility and has been marked as deprecated.
Documented the new associated functions of the ValidationErrors
implementation and removed unnecessary feature declaration in a test
module.
Refactored tests to use the new `field_errors` accessor method.
Updated README.md to describe nested validation behaviour.
Keats/validator#31
Diffstat (limited to 'validator_derive/src')
| -rw-r--r-- | validator_derive/src/lib.rs | 17 | ||||
| -rw-r--r-- | validator_derive/src/quoting.rs | 70 |
2 files changed, 67 insertions, 20 deletions
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()); |
