diff options
| author | Vincent Prouillet | 2017-07-19 16:30:01 +0900 |
|---|---|---|
| committer | GitHub | 2017-07-19 16:30:01 +0900 |
| commit | 2ab1a0fa0fe8fe9ccc025cbd7228e6b8201b1f1f (patch) | |
| tree | e71b13aea39c61b19e8aec09468a8e5c4a291e16 | |
| parent | 69e35d6cc905b9d7a894af7e486237e376fae939 (diff) | |
| parent | b31fd25cc5cec96ccee737ba1313c6e9f702f32a (diff) | |
| download | validator-2ab1a0fa0fe8fe9ccc025cbd7228e6b8201b1f1f.tar.bz2 | |
Merge pull request #27 from Keats/revamp
Complete refactor
35 files changed, 1985 insertions, 1048 deletions
diff --git a/validator/Cargo.toml b/validator/Cargo.toml index c20c1ac..3565fef 100644 --- a/validator/Cargo.toml +++ b/validator/Cargo.toml @@ -13,3 +13,6 @@ url = "1" regex = "0.2" lazy_static = "0.2" idna = "0.1" +serde = "1" +serde_derive = "1" +serde_json = "1" diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 2e44b2e..35afbe4 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -1,24 +1,25 @@ extern crate url; extern crate regex; -#[macro_use] extern crate lazy_static; +#[macro_use] +extern crate lazy_static; extern crate idna; - +extern crate serde; +extern crate serde_json; +#[macro_use] +extern crate serde_derive; mod types; -mod ip; -mod email; -mod length; -mod range; -mod urls; -mod must_match; -mod contains; +mod validation; +mod traits; +pub use validation::ip::{validate_ip, validate_ip_v4, validate_ip_v6}; +pub use validation::email::{validate_email}; +pub use validation::length::{validate_length}; +pub use validation::range::{validate_range}; +pub use validation::urls::{validate_url}; +pub use validation::must_match::{validate_must_match}; +pub use validation::contains::{validate_contains}; +pub use validation::Validator; -pub use types::{Errors, Validate, Validator}; -pub use ip::{validate_ip, validate_ip_v4, validate_ip_v6}; -pub use email::{validate_email}; -pub use length::{HasLen, validate_length}; -pub use range::{validate_range}; -pub use urls::{validate_url}; -pub use must_match::{validate_must_match}; -pub use contains::{Contains, validate_contains}; +pub use types::{ValidationErrors, ValidationError}; +pub use traits::{Validate, HasLen, Contains}; diff --git a/validator/src/traits.rs b/validator/src/traits.rs new file mode 100644 index 0000000..896fcc7 --- /dev/null +++ b/validator/src/traits.rs @@ -0,0 +1,82 @@ +use std::collections::HashMap; + +use types::ValidationErrors; + + +/// Trait to implement if one wants to make the `length` validator +/// work for more types +/// +/// A bit sad it's not there by default in Rust +pub trait HasLen { + fn length(&self) -> u64; +} + +impl HasLen for String { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> HasLen for &'a String { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> HasLen for &'a str { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<T> HasLen for Vec<T> { + fn length(&self) -> u64 { + self.len() as u64 + } +} +impl<'a, T> HasLen for &'a Vec<T> { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +/// Trait to implement if one wants to make the `contains` validator +/// work for more types +pub trait Contains { + fn has_element(&self, needle: &str) -> bool; +} + +impl Contains for String { + fn has_element(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl<'a> Contains for &'a String { + fn has_element(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl<'a> Contains for &'a str { + fn has_element(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl<S> Contains for HashMap<String, S> { + fn has_element(&self, needle: &str) -> bool { + self.contains_key(needle) + } +} + +impl<'a, S> Contains for &'a HashMap<String, S> { + fn has_element(&self, needle: &str) -> bool { + self.contains_key(needle) + } +} + +/// The trait that `validator_derive` implements +pub trait Validate { + fn validate(&self) -> Result<(), ValidationErrors>; +} diff --git a/validator/src/types.rs b/validator/src/types.rs index 0a7c2fa..bb176c3 100644 --- a/validator/src/types.rs +++ b/validator/src/types.rs @@ -1,85 +1,61 @@ use std::{self, fmt}; +use std::borrow::Cow; use std::collections::HashMap; -#[derive(Debug)] -pub struct Errors(HashMap<String, Vec<String>>); +use serde_json::{Value, to_value}; +use serde::ser::Serialize; -impl Errors { - pub fn new() -> Errors { - Errors(HashMap::new()) - } - pub fn inner(self) -> HashMap<String, Vec<String>> { - self.0 - } +#[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 fn add(&mut self, field: &str, err: &str) { - self.0.entry(field.to_string()).or_insert_with(|| vec![]).push(err.to_string()); +impl ValidationError { + pub fn new(code: &'static str) -> ValidationError { + ValidationError { + code: Cow::from(code), + message: None, + params: HashMap::new(), + } } - pub fn is_empty(&self) -> bool { - self.0.is_empty() + pub fn add_param<T: Serialize>(&mut self, name: Cow<'static, str>, val: &T) { + self.params.insert(name, to_value(val).unwrap()); } } -impl fmt::Display for Errors { +impl fmt::Display for ValidationError { fn fmt(&self, fmt: &mut fmt::Formatter) -> fmt::Result { - write!(fmt, "Validation failed:\n")?; - for (field, errs) in &self.0 { - write!(fmt, " {}: [", field)?; - - let last = errs.len() - 1; - for (index, err) in errs.iter().enumerate() { - write!(fmt, "{}", err)?; - if index < last { write!(fmt, ", ")? } - } - write!(fmt, "]\n")?; - } - Ok(()) + write!(fmt, "Validation error: {} [{:?}]", self.code, self.params) } } -impl std::error::Error for Errors { - fn description(&self) -> &str { - "validation failed" +impl std::error::Error for ValidationError { + fn description(&self) -> &str { &self.code } + fn cause(&self) -> Option<&std::error::Error> { None } +} + +#[derive(Debug, Serialize, Clone, PartialEq)] +pub struct ValidationErrors(HashMap<&'static str, Vec<ValidationError>>); + + +impl ValidationErrors { + pub fn new() -> ValidationErrors { + ValidationErrors(HashMap::new()) } - fn cause(&self) -> Option<&std::error::Error> { - None + pub fn inner(self) -> HashMap<&'static str, Vec<ValidationError>> { + self.0 } -} -pub trait Validate { - fn validate(&self) -> Result<(), Errors>; -} + pub fn add(&mut self, field: &'static str, error: ValidationError) { + self.0.entry(field).or_insert_with(|| vec![]).push(error); + } -/// Contains all the validators that can be used -/// -/// In this crate as it's not allowed to export more than the proc macro -/// in a proc macro crate -#[derive(Debug, Clone)] -pub enum Validator { - // String is the path to the function - Custom(String), - // String is the name of the field to match - MustMatch(String), - // value is a &str - Email, - // value is a &str - Url, - // value is a &str or a HashMap<String, ..> - Contains(String), - // value is a &str - Regex(String), - // value is a number - Range { - min: f64, - max: f64, - }, - // value is anything that impl HasLen - Length { - min: Option<u64>, - max: Option<u64>, - equal: Option<u64>, - }, + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } } diff --git a/validator/src/contains.rs b/validator/src/validation/contains.rs index 0c9d02e..58baabe 100644 --- a/validator/src/contains.rs +++ b/validator/src/validation/contains.rs @@ -1,41 +1,4 @@ -use std::collections::HashMap; - - -/// Trait to implement if one wants to make the `contains` validator -/// work for more types -pub trait Contains { - fn has_element(&self, needle: &str) -> bool; -} - -impl Contains for String { - fn has_element(&self, needle: &str) -> bool { - self.contains(needle) - } -} - -impl<'a> Contains for &'a String { - fn has_element(&self, needle: &str) -> bool { - self.contains(needle) - } -} - -impl<'a> Contains for &'a str { - fn has_element(&self, needle: &str) -> bool { - self.contains(needle) - } -} - -impl<S> Contains for HashMap<String, S> { - fn has_element(&self, needle: &str) -> bool { - self.contains_key(needle) - } -} - -impl<'a, S> Contains for &'a HashMap<String, S> { - fn has_element(&self, needle: &str) -> bool { - self.contains_key(needle) - } -} +use traits::Contains; /// Validates whether the value contains the needle /// The value needs to implement the Contains trait, which is implement on String, str and Hashmap<String> diff --git a/validator/src/email.rs b/validator/src/validation/email.rs index 205d89f..cf6adc5 100644 --- a/validator/src/email.rs +++ b/validator/src/validation/email.rs @@ -1,7 +1,7 @@ use regex::Regex; +use idna::domain_to_ascii; -use ip::{validate_ip}; -use idna::{domain_to_ascii}; +use validation::ip::validate_ip; lazy_static! { diff --git a/validator/src/ip.rs b/validator/src/validation/ip.rs index 9a94522..9a94522 100644 --- a/validator/src/ip.rs +++ b/validator/src/validation/ip.rs diff --git a/validator/src/length.rs b/validator/src/validation/length.rs index 965d43b..1c3affc 100644 --- a/validator/src/length.rs +++ b/validator/src/validation/length.rs @@ -1,41 +1,5 @@ -use types::Validator; - -/// Trait to implement if one wants to make the `length` validator -/// work for more types -/// -/// A bit sad it's not there by default in Rust -pub trait HasLen { - fn length(&self) -> u64; -} - -impl HasLen for String { - fn length(&self) -> u64 { - self.chars().count() as u64 - } -} - -impl<'a> HasLen for &'a String { - fn length(&self) -> u64 { - self.chars().count() as u64 - } -} - -impl<'a> HasLen for &'a str { - fn length(&self) -> u64 { - self.chars().count() as u64 - } -} - -impl<T> HasLen for Vec<T> { - fn length(&self) -> u64 { - self.len() as u64 - } -} -impl<'a, T> HasLen for &'a Vec<T> { - fn length(&self) -> u64 { - self.len() as u64 - } -} +use validation::Validator; +use traits::HasLen; /// Validates the length of the value given. /// If the validator has `equal` set, it will ignore any `min` and `max` value. diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs new file mode 100644 index 0000000..939cacf --- /dev/null +++ b/validator/src/validation/mod.rs @@ -0,0 +1,50 @@ +pub mod ip; +pub mod email; +pub mod length; +pub mod range; +pub mod urls; +pub mod must_match; +pub mod contains; + +/// Contains all the validators that can be used +/// +/// In this crate as it's not allowed to export more than the proc macro +/// in a proc macro crate +#[derive(Debug, Clone, PartialEq)] +pub enum Validator { + Email, + Url, + // String is the path to the function + Custom(String), + // String is the name of the field to match + MustMatch(String), + // value is a &str or a HashMap<String, ..> + Contains(String), + // No implementation in this crate, it's all in validator_derive + Regex(String), + Range { + min: f64, + max: f64, + }, + // Any value that impl HasLen can be validated with Length + Length { + min: Option<u64>, + max: Option<u64>, + equal: Option<u64>, + }, +} + +impl Validator { + pub fn code(&self) -> &'static str { + match *self { + Validator::MustMatch(_) => "must_match", + Validator::Email => "email", + Validator::Url => "url", + Validator::Custom(_) => "custom", + Validator::Contains(_) => "contains", + Validator::Regex(_) => "regex", + Validator::Range {..} => "range", + Validator::Length {..} => "length", + } + } +} diff --git a/validator/src/must_match.rs b/validator/src/validation/must_match.rs index 35fbee6..35fbee6 100644 --- a/validator/src/must_match.rs +++ b/validator/src/validation/must_match.rs diff --git a/validator/src/range.rs b/validator/src/validation/range.rs index 8db9a86..8cb9ec7 100644 --- a/validator/src/range.rs +++ b/validator/src/validation/range.rs @@ -1,4 +1,4 @@ -use types::Validator; +use validation::Validator; /// Validates that a number is in the given range /// diff --git a/validator/src/urls.rs b/validator/src/validation/urls.rs index d948050..d948050 100644 --- a/validator/src/urls.rs +++ b/validator/src/validation/urls.rs diff --git a/validator_derive/Cargo.toml b/validator_derive/Cargo.toml index d5a1f5e..127ae9e 100644 --- a/validator_derive/Cargo.toml +++ b/validator_derive/Cargo.toml @@ -14,6 +14,7 @@ proc-macro = true [dependencies] syn = "0.11" quote = "0.3" +if_chain = "0" [dev-dependencies] serde = "1.0" @@ -24,5 +25,5 @@ regex = "0.2" lazy_static = "0.2" [dependencies.validator] -# path = "../validator" -version = "0.4.0" +path = "../validator" +# version = "0.4.0" diff --git a/validator_derive/src/asserts.rs b/validator_derive/src/asserts.rs new file mode 100644 index 0000000..65db5cf --- /dev/null +++ b/validator_derive/src/asserts.rs @@ -0,0 +1,54 @@ + +pub static NUMBER_TYPES: [&'static str; 24] = [ + "usize", "u8", "u16", "u32", "u64", + "isize", "i8", "i16", "i32", "i64", + "f32", "f64", + + "Option<usize>", "Option<u8>", "Option<u16>", "Option<u32>", "Option<u64>", + "Option<isize>", "Option<i8>", "Option<i16>", "Option<i32>", "Option<i64>", + "Option<f32>", "Option<f64>", +]; + + +pub fn assert_string_type(name: &str, field_type: &String) { + if field_type != "String" + && field_type != "&str" + && field_type != "Option<String>" + && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) { + panic!("`{}` validator can only be used on String, &str or an Option of those", name); + } +} + +pub fn assert_type_matches(field_name: String, field_type: &String, field_type2: Option<&String>) { + if let Some(t2) = field_type2 { + if field_type != t2 { + panic!("Invalid argument for `must_match` validator of field `{}`: types of field can't match", field_name); + } + } else { + panic!("Invalid argument for `must_match` validator of field `{}`: the other field doesn't exist in struct", field_name); + } +} + +pub fn assert_has_len(field_name: String, field_type: &String) { + if field_type != "String" + && !field_type.starts_with("Vec<") + && !field_type.starts_with("Option<Vec<") + && field_type != "Option<String>" + // a bit ugly + && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) + && field_type != "&str" { + panic!( + "Validator `length` can only be used on types `String`, `&str` or `Vec` but found `{}` for field `{}`", + field_type, field_name + ); + } +} + +pub fn assert_has_range(field_name: String, field_type: &String) { + if !NUMBER_TYPES.contains(&field_type.as_ref()) { + panic!( + "Validator `range` can only be used on number types but found `{}` for field `{}`", + field_type, field_name + ); + } +} diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index 6bc8313..1298006 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -1,33 +1,31 @@ #![recursion_limit = "128"] -#[macro_use] extern crate quote; +#[macro_use] +extern crate quote; extern crate proc_macro; extern crate syn; +#[macro_use] +extern crate if_chain; extern crate validator; use std::collections::HashMap; use proc_macro::TokenStream; use quote::ToTokens; -use validator::{Validator}; +use validator::Validator; -static RANGE_TYPES: [&'static str; 24] = [ - "usize", "u8", "u16", "u32", "u64", - "isize", "i8", "i16", "i32", "i64", - "f32", "f64", - "Option<usize>", "Option<u8>", "Option<u16>", "Option<u32>", "Option<u64>", - "Option<isize>", "Option<i8>", "Option<i16>", "Option<i32>", "Option<i64>", - "Option<f32>", "Option<f64>", -]; +mod lit; +mod validation; +mod asserts; +mod quoting; -#[derive(Debug)] -struct SchemaValidation { - function: String, - skip_on_field_errors: bool, -} +use lit::*; +use validation::*; +use asserts::{assert_string_type, assert_type_matches, assert_has_len, assert_has_range}; +use quoting::{FieldQuoter, quote_field_validation, quote_schema_validation}; #[proc_macro_derive(Validate, attributes(validate))] @@ -36,12 +34,13 @@ pub fn derive_validation(input: TokenStream) -> TokenStream { // Parse the string representation to an AST let ast = syn::parse_macro_input(&source).unwrap(); - let expanded = expand_validation(&ast); + let expanded = impl_validate(&ast); expanded.parse().unwrap() } -fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { +fn impl_validate(ast: &syn::MacroInput) -> quote::Tokens { + // Ensure the macro is on a struct with named fields let fields = match ast.body { syn::Body::Struct(syn::VariantData::Struct(ref fields)) => { if fields.iter().any(|field| field.ident.is_none()) { @@ -57,228 +56,17 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { let field_types = find_fields_type(&fields); for field in fields { - let field_ident = match field.ident { - Some(ref i) => i, - None => unreachable!() - }; - - let (name, validators) = find_validators_for_field(field, &field_types); - let field_name = field_types.get(&field_ident.to_string()).unwrap(); - // Don't put a & in front a pointer - let validator_param = if field_name.starts_with("&") { - quote!(self.#field_ident) - } else { - quote!(&self.#field_ident) - }; - // same but for the ident used in a if let block - let optional_validator_param = quote!(#field_ident); - // same but for the ident used in a if let Some variable - let optional_pattern_matched = if field_name.starts_with("Option<&") { - quote!(#field_ident) - } else { - quote!(ref #field_ident) - }; + let field_ident = field.ident.clone().unwrap(); + let (name, field_validations) = find_validators_for_field(field, &field_types); + let field_type = field_types.get(&field_ident.to_string()).cloned().unwrap(); + let field_quoter = FieldQuoter::new(field_ident, name, field_type); - for validator in &validators { - validations.push(match validator { - &Validator::Length {min, max, equal} => { - // Can't interpolate None - let min_tokens = option_u64_to_tokens(min); - let max_tokens = option_u64_to_tokens(max); - let equal_tokens = option_u64_to_tokens(equal); - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_length( - ::validator::Validator::Length { - min: #min_tokens, - max: #max_tokens, - equal: #equal_tokens - }, - #optional_validator_param - ) { - errors.add(#name, "length"); - } - } - ) - } else { - quote!( - if !::validator::validate_length( - ::validator::Validator::Length { - min: #min_tokens, - max: #max_tokens, - equal: #equal_tokens - }, - #validator_param - ) { - errors.add(#name, "length"); - } - ) - } - }, - &Validator::Range {min, max} => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#field_ident) = self.#field_ident { - if !::validator::validate_range( - ::validator::Validator::Range {min: #min, max: #max}, - #field_ident as f64 - ) { - errors.add(#name, "range"); - } - } - ) - } else { - quote!( - if !::validator::validate_range( - ::validator::Validator::Range {min: #min, max: #max}, - self.#field_ident as f64 - ) { - errors.add(#name, "range"); - } - ) - } - }, - &Validator::Email => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_email(#optional_validator_param) { - errors.add(#name, "email"); - } - } - ) - } else { - quote!( - if !::validator::validate_email(#validator_param) { - errors.add(#name, "email"); - } - ) - } - } - &Validator::Url => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_url(#optional_validator_param) { - errors.add(#name, "url"); - } - } - ) - } else { - quote!( - if !::validator::validate_url(#validator_param) { - errors.add(#name, "url"); - } - ) - } - }, - &Validator::MustMatch(ref f) => { - let other_ident = syn::Ident::new(f.clone()); - quote!( - if !::validator::validate_must_match(&self.#field_ident, &self.#other_ident) { - errors.add(#name, "no_match"); - } - ) - }, - &Validator::Custom(ref f) => { - let fn_ident = syn::Ident::new(f.clone()); - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - match #fn_ident(#optional_validator_param) { - ::std::option::Option::Some(s) => { - errors.add(#name, &s); - }, - ::std::option::Option::None => (), - }; - } - ) - } else { - quote!( - match #fn_ident(#validator_param) { - ::std::option::Option::Some(s) => { - errors.add(#name, &s); - }, - ::std::option::Option::None => (), - }; - ) - } - }, - &Validator::Contains(ref n) => { - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !::validator::validate_contains(#optional_validator_param, &#n) { - errors.add(#name, "contains"); - } - } - ) - } else { - quote!( - if !::validator::validate_contains(#validator_param, &#n) { - errors.add(#name, "contains"); - } - ) - } - }, - &Validator::Regex(ref re) => { - let re_ident = syn::Ident::new(re.clone()); - // wrap in if-let if we have an option - if field_name.starts_with("Option<") { - quote!( - if let Some(#optional_pattern_matched) = self.#field_ident { - if !#re_ident.is_match(#optional_validator_param) { - errors.add(#name, "regex"); - } - } - ) - } else { - quote!( - if !#re_ident.is_match(#validator_param) { - errors.add(#name, "regex"); - } - ) - } - }, - }); + for validation in &field_validations { + validations.push(quote_field_validation(&field_quoter, validation)); } } - let struct_validation = find_struct_validation(&ast.attrs); - let struct_validation_tokens = match struct_validation { - Some(s) => { - let fn_ident = syn::Ident::new(s.function); - if s.skip_on_field_errors { - quote!( - if errors.is_empty() { - match #fn_ident(self) { - ::std::option::Option::Some((key, val)) => { - errors.add(&key, &val); - }, - ::std::option::Option::None => (), - } - } - ) - } else { - quote!( - match #fn_ident(self) { - ::std::option::Option::Some((key, val)) => { - errors.add(&key, &val); - }, - ::std::option::Option::None => (), - } - ) - } - }, - None => quote!() - }; + let schema_validation = quote_schema_validation(find_struct_validation(&ast.attrs)); let ident = &ast.ident; @@ -286,12 +74,12 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl(); let impl_ast = quote!( impl #impl_generics Validate for #ident #ty_generics #where_clause { - fn validate(&self) -> ::std::result::Result<(), ::validator::Errors> { - let mut errors = ::validator::Errors::new(); + fn validate(&self) -> ::std::result::Result<(), ::validator::ValidationErrors> { + let mut errors = ::validator::ValidationErrors::new(); #(#validations)* - #struct_validation_tokens + #schema_validation if errors.is_empty() { ::std::result::Result::Ok(()) @@ -316,61 +104,80 @@ fn find_struct_validation(struct_attrs: &Vec<syn::Attribute>) -> Option<SchemaVa if attr.value.name() != "validate" { continue; } - match attr.value { - syn::MetaItem::List(_, ref meta_items) => { - match meta_items[0] { - syn::NestedMetaItem::MetaItem(ref item) => match item { - &syn::MetaItem::List(ref ident2, ref args) => { - if ident2 != "schema" { - error("Only `schema` is allowed as validator on a struct") - } - let mut function = "".to_string(); - let mut skip_on_field_errors = true; - for arg in args { - match *arg { - syn::NestedMetaItem::MetaItem(ref item) => match *item { - syn::MetaItem::NameValue(ref name, ref val) => { - match name.to_string().as_ref() { - "function" => { - function = match lit_to_string(val) { - Some(s) => s, - None => error("invalid argument type for `function` \ - : only a string is allowed"), - }; - }, - "skip_on_field_errors" => { - skip_on_field_errors = match lit_to_bool(val) { - Some(s) => s, - None => error("invalid argument type for `skip_on_field_errors` \ - : only a bool is allowed"), - }; - }, - _ => error("Unknown argument") - } - - }, - _ => error("Unexpected args") - }, - _ => error("Unexpected args") - } - } + if_chain! { + if let syn::MetaItem::List(_, ref meta_items) = attr.value; + if let syn::NestedMetaItem::MetaItem(ref item) = meta_items[0]; + if let &syn::MetaItem::List(ref ident2, ref args) = item; + + then { + if ident2 != "schema" { + error("Only `schema` is allowed as validator on a struct") + } - if function == "" { - error("`function` is required"); + let mut function = String::new(); + let mut skip_on_field_errors = true; + let mut code = None; + let mut message = None; + + for arg in args { + if_chain! { + if let syn::NestedMetaItem::MetaItem(ref item) = *arg; + if let syn::MetaItem::NameValue(ref name, ref val) = *item; + + then { + match name.to_string().as_ref() { + "function" => { + function = match lit_to_string(val) { + Some(s) => s, + None => error("invalid argument type for `function` \ + : only a string is allowed"), + }; + }, + "skip_on_field_errors" => { + skip_on_field_errors = match lit_to_bool(val) { + Some(s) => s, + None => error("invalid argument type for `skip_on_field_errors` \ + : only a bool is allowed"), + }; + }, + "code" => { + code = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `code` \ + : only a string is allowed"), + }; + }, + "message" => { + message = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `message` \ + : only a string is allowed"), + }; + }, + _ => error("Unknown argument") } + } else { + error("Unexpected args") + } + } + } - return Some(SchemaValidation { - function: function, - skip_on_field_errors: skip_on_field_errors - }); - }, - _ => error("Unexpected struct validator") - }, - _ => error("Unexpected struct validator") + if function == "" { + error("`function` is required"); } - }, - _ => error("Unexpected struct validator") + + return Some( + SchemaValidation { + function, + skip_on_field_errors, + code, + message, + } + ); + } else { + error("Unexpected struct validator") + } } } @@ -378,16 +185,13 @@ fn find_struct_validation(struct_attrs: &Vec<syn::Attribute>) -> Option<SchemaVa } -// Find all the types (as string) for each field of the struct -// Needed for the `must_match` filter +/// Find the types (as string) for each field of the struct +/// Needed for the `must_match` filter fn find_fields_type(fields: &Vec<syn::Field>) -> HashMap<String, String> { let mut types = HashMap::new(); for field in fields { - let field_name = match field.ident { - Some(ref s) => s.to_string(), - None => unreachable!(), - }; + let field_ident = field.ident.clone().unwrap().to_string(); let field_type = match field.ty { syn::Ty::Path(_, ref p) => { let mut tokens = quote::Tokens::new(); @@ -404,114 +208,31 @@ fn find_fields_type(fields: &Vec<syn::Field>) -> HashMap<String, String> { } name }, - _ => panic!("Type `{:?}` of field `{}` not supported", field.ty, field_name) + _ => panic!("Type `{:?}` of field `{}` not supported", field.ty, field_ident) }; + //println!("{:?}", field_type); - types.insert(field_name, field_type); + types.insert(field_ident, field_type); } types } -/// Find everything we need to know about a Field. -fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, String>) -> (String, Vec<Validator>) { - let mut field_name = match field.ident { - Some(ref s) => s.to_string(), - None => unreachable!(), - }; +/// Find everything we need to know about a field: its real name if it's changed from the serialization +/// and the list of validators to run on it +fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, String>) -> (String, Vec<FieldValidation>) { + let rust_ident = field.ident.clone().unwrap().to_string(); + let mut field_ident = field.ident.clone().unwrap().to_string(); let error = |msg: &str| -> ! { panic!("Invalid attribute #[validate] on field `{}`: {}", field.ident.clone().unwrap().to_string(), msg); }; - let field_type = field_types.get(&field_name).unwrap(); + + let field_type = field_types.get(&field_ident).unwrap(); let mut validators = vec![]; let mut has_validate = false; - let find_struct_validator = |name: String, meta_items: &Vec<syn::NestedMetaItem>| -> Validator { - match name.as_ref() { - "length" => { - let mut min = None; - let mut max = None; - let mut equal = None; - - for meta_item in meta_items { - match *meta_item { - syn::NestedMetaItem::MetaItem(ref item) => match *item { - syn::MetaItem::NameValue(ref name, ref val) => { - match name.to_string().as_ref() { - "min" => { - min = match lit_to_int(val) { - Some(s) => Some(s), - None => error("invalid argument type for `min` of `length` validator: only integers are allowed"), - }; - }, - "max" => { - max = match lit_to_int(val) { - Some(s) => Some(s), - None => error("invalid argument type for `max` of `length` validator: only integers are allowed"), - }; - }, - "equal" => { - equal = match lit_to_int(val) { - Some(s) => Some(s), - None => error("invalid argument type for `equal` of `length` validator: only integers are allowed"), - }; - }, - _ => error(&format!( - "unknown argument `{}` for validator `length` (it only has `min`, `max`, `equal`)", - name.to_string() - )) - } - }, - _ => panic!("unexpected item {:?} while parsing `length` validator", item) - }, - _=> unreachable!() - } - } - if equal.is_some() && (min.is_some() || max.is_some()) { - error("both `equal` and `min` or `max` have been set in `length` validator: probably a mistake"); - } - Validator::Length { min: min, max: max, equal: equal } - }, - "range" => { - let mut min = 0.0; - let mut max = 0.0; - for meta_item in meta_items { - match *meta_item { - syn::NestedMetaItem::MetaItem(ref item) => match *item { - syn::MetaItem::NameValue(ref name, ref val) => { - match name.to_string().as_ref() { - "min" => { - min = match lit_to_float(val) { - Some(s) => s, - None => error("invalid argument type for `min` of `range` validator: only integers are allowed") - }; - }, - "max" => { - max = match lit_to_float(val) { - Some(s) => s, - None => error("invalid argument type for `max` of `range` validator: only integers are allowed") - }; - }, - _ => error(&format!( - "unknown argument `{}` for validator `range` (it only has `min`, `max`)", - name.to_string() - )) - } - }, - _ => panic!("unexpected item {:?} while parsing `range` validator", item) - }, - _=> unreachable!() - } - } - - Validator::Range { min: min, max: max} - } - _ => panic!("unexpected list validator: {:?}", name) - } - }; - for attr in &field.attrs { if attr.name() != "validate" && attr.name() != "serde" { continue; @@ -523,11 +244,11 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S match attr.value { syn::MetaItem::List(_, ref meta_items) => { + // original name before serde rename if attr.name() == "serde" { - match find_original_field_name(meta_items) { - Some(s) => { field_name = s }, - None => () - }; + if let Some(s) = find_original_field_name(meta_items) { + field_ident = s; + } continue; } @@ -538,58 +259,41 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S // email, url syn::MetaItem::Word(ref name) => match name.to_string().as_ref() { "email" => { - if field_type != "String" - && field_type != "&str" - && field_type != "Option<String>" - && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) { - panic!("`email` validator can only be used on String or &str"); - } - validators.push(Validator::Email); + assert_string_type("email", field_type); + validators.push(FieldValidation::new(Validator::Email)); }, "url" => { - if field_type != "String" - && field_type != "&str" - && field_type != "Option<String>" - && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) { - panic!("`url` validator can only be used on String or &str"); - } - validators.push(Validator::Url); + assert_string_type("url", field_type); + validators.push(FieldValidation::new(Validator::Url)); }, - _ => panic!("Unexpected word validator: {}", name) + _ => panic!("Unexpected validator: {}", name) }, - // custom, contains, must_match + // custom, contains, must_match, regex syn::MetaItem::NameValue(ref name, ref val) => { match name.to_string().as_ref() { "custom" => { match lit_to_string(val) { - Some(s) => validators.push(Validator::Custom(s)), + Some(s) => validators.push(FieldValidation::new(Validator::Custom(s))), None => error("invalid argument for `custom` validator: only strings are allowed"), }; }, "contains" => { match lit_to_string(val) { - Some(s) => validators.push(Validator::Contains(s)), + Some(s) => validators.push(FieldValidation::new(Validator::Contains(s))), None => error("invalid argument for `contains` validator: only strings are allowed"), }; }, "regex" => { match lit_to_string(val) { - Some(s) => validators.push(Validator::Regex(s)), + Some(s) => validators.push(FieldValidation::new(Validator::Regex(s))), None => error("invalid argument for `regex` validator: only strings are allowed"), }; } "must_match" => { match lit_to_string(val) { Some(s) => { - if let Some(t2) = field_types.get(&s) { - if field_type == t2 { - validators.push(Validator::MustMatch(s)); - } else { - error("invalid argument for `must_match` validator: types of field can't match"); - } - } else { - error("invalid argument for `must_match` validator: field doesn't exist in struct"); - } + assert_type_matches(rust_ident.clone(), field_type, field_types.get(&s)); + validators.push(FieldValidation::new(Validator::MustMatch(s))); }, None => error("invalid argument for `must_match` validator: only strings are allowed"), }; @@ -597,49 +301,43 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S _ => panic!("unexpected name value validator: {:?}", name), }; }, - // validators with args: length for example - syn::MetaItem::List(ref name, ref meta_items) => { - // Some sanity checking first - if name == "length" { - if field_type != "String" - && !field_type.starts_with("Vec<") - && !field_type.starts_with("Option<Vec<") - && field_type != "Option<String>" - // a bit ugly - && !(field_type.starts_with("Option<") && field_type.ends_with("str>")) - && field_type != "&str" { - error(&format!( - "Validator `length` can only be used on types `String`, `&str` or `Vec` but found `{}`", - field_type - )); - } - - if meta_items.len() == 0 { - error("Validator `length` requires at least 1 argument out of `min`, `max` and `equal`"); - } - } - - if name == "range" { - if !RANGE_TYPES.contains(&field_type.as_ref()) { - error(&format!( - "Validator `range` can only be used on number types but found `{}`", - field_type - )); - } - - if meta_items.len() != 2 { - error("Validator `range` requires 2 arguments: `min` and `max`"); + // Validators with several args + syn::MetaItem::List(ref name, ref meta_items) => match name.to_string().as_ref() { + "length" => { + assert_has_len(rust_ident.clone(), field_type); + validators.push(extract_length_validation(rust_ident.clone(), meta_items)); + }, + "range" => { + assert_has_range(rust_ident.clone(), field_type); + validators.push(extract_range_validation(rust_ident.clone(), meta_items)); + }, + "email" | "url" => { + validators.push(extract_argless_validation(name.to_string(), rust_ident.clone(), meta_items)); + }, + "custom" => { + validators.push(extract_one_arg_validation("function", name.to_string(), rust_ident.clone(), meta_items)); + }, + "contains" => { + validators.push(extract_one_arg_validation("pattern", name.to_string(), rust_ident.clone(), meta_items)); + }, + "regex" => { + validators.push(extract_one_arg_validation("path", name.to_string(), rust_ident.clone(), meta_items)); + }, + "must_match" => { + let validation = extract_one_arg_validation("other", name.to_string(), rust_ident.clone(), meta_items); + if let Validator::MustMatch(ref t2) = validation.validator { + assert_type_matches(rust_ident.clone(), field_type, field_types.get(t2)); } - } - - validators.push(find_struct_validator(name.to_string(), meta_items)); + validators.push(validation); + }, + _ => panic!("unexpected list validator: {:?}", name.to_string()) }, }, _ => unreachable!("Found a non MetaItem while looking for validators") }; } }, - _ => unreachable!("Got something other than a list of attributes while checking field `{}`", field_name), + _ => unreachable!("Got something other than a list of attributes while checking field `{}`", field_ident), } } @@ -647,14 +345,14 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S error("it needs at least one validator"); } - (field_name, validators) + (field_ident, validators) } /// Serde can be used to rename fields on deserialization but most of the times /// we want the error on the original field. /// /// For example a JS frontend might send camelCase fields and Rust converts them to snake_case -/// but we want to send the errors back to the frontend with the original name +/// but we want to send the errors back with the original name fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<String> { let mut original_name = None; @@ -666,9 +364,7 @@ fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<Str if name == "rename" { original_name = Some(lit_to_string(val).unwrap()); } - }, - // length syn::MetaItem::List(_, ref meta_items) => { return find_original_field_name(meta_items); } @@ -684,61 +380,3 @@ fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<Str original_name } - -fn lit_to_string(lit: &syn::Lit) -> Option<String> { - match *lit { - syn::Lit::Str(ref s, _) => Some(s.to_string()), - _ => None, - } -} - -fn lit_to_int(lit: &syn::Lit) -> Option<u64> { - match *lit { - syn::Lit::Int(ref s, _) => Some(*s), - // TODO: remove when attr_literals is stable - syn::Lit::Str(ref s, _) => Some(s.parse::<u64>().unwrap()), - _ => None, - } -} - -fn lit_to_float(lit: &syn::Lit) -> Option<f64> { - match *lit { - syn::Lit::Float(ref s, _) => Some(s.parse::<f64>().unwrap()), - syn::Lit::Int(ref s, _) => Some(*s as f64), - // TODO: remove when attr_literals is stable - syn::Lit::Str(ref s, _) => Some(s.parse::<f64>().unwrap()), - _ => None, - } -} - -fn lit_to_bool(lit: &syn::Lit) -> Option<bool> { - match *lit { - syn::Lit::Bool(ref s) => Some(*s), - // TODO: remove when attr_literals is stable - syn::Lit::Str(ref s, _) => if s == "true" { Some(true) } else { Some(false) }, - _ => None, - } -} - -fn option_u64_to_tokens(opt: Option<u64>) -> quote::Tokens { - let mut tokens = quote::Tokens::new(); - tokens.append("::"); - tokens.append("std"); - tokens.append("::"); - tokens.append("option"); - tokens.append("::"); - tokens.append("Option"); - tokens.append("::"); - match opt { - Some(ref t) => { - tokens.append("Some"); - tokens.append("("); - t.to_tokens(&mut tokens); - tokens.append(")"); - } - None => { - tokens.append("None"); - } - } - tokens -} diff --git a/validator_derive/src/lit.rs b/validator_derive/src/lit.rs new file mode 100644 index 0000000..0753a17 --- /dev/null +++ b/validator_derive/src/lit.rs @@ -0,0 +1,61 @@ +use quote::{self, ToTokens}; +use syn; + + +pub fn lit_to_string(lit: &syn::Lit) -> Option<String> { + match *lit { + syn::Lit::Str(ref s, _) => Some(s.to_string()), + _ => None, + } +} + +pub fn lit_to_int(lit: &syn::Lit) -> Option<u64> { + match *lit { + syn::Lit::Int(ref s, _) => Some(*s), + // TODO: remove when attr_literals is stable + syn::Lit::Str(ref s, _) => Some(s.parse::<u64>().unwrap()), + _ => None, + } +} + +pub fn lit_to_float(lit: &syn::Lit) -> Option<f64> { + match *lit { + syn::Lit::Float(ref s, _) => Some(s.parse::<f64>().unwrap()), + syn::Lit::Int(ref s, _) => Some(*s as f64), + // TODO: remove when attr_literals is stable + syn::Lit::Str(ref s, _) => Some(s.parse::<f64>().unwrap()), + _ => None, + } +} + +pub fn lit_to_bool(lit: &syn::Lit) -> Option<bool> { + match *lit { + syn::Lit::Bool(ref s) => Some(*s), + // TODO: remove when attr_literals is stable + syn::Lit::Str(ref s, _) => if s == "true" { Some(true) } else { Some(false) }, + _ => None, + } +} + +pub fn option_u64_to_tokens(opt: Option<u64>) -> quote::Tokens { + let mut tokens = quote::Tokens::new(); + tokens.append("::"); + tokens.append("std"); + tokens.append("::"); + tokens.append("option"); + tokens.append("::"); + tokens.append("Option"); + tokens.append("::"); + match opt { + Some(ref t) => { + tokens.append("Some"); + tokens.append("("); + t.to_tokens(&mut tokens); + tokens.append(")"); + } + None => { + tokens.append("None"); + } + } + tokens +} diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs new file mode 100644 index 0000000..69353e9 --- /dev/null +++ b/validator_derive/src/quoting.rs @@ -0,0 +1,332 @@ +use quote; +use validator::Validator; +use syn; + +use lit::option_u64_to_tokens; +use validation::{FieldValidation, SchemaValidation}; +use asserts::NUMBER_TYPES; + + +/// Pass around all the information needed for creating a validation +#[derive(Debug)] +pub struct FieldQuoter { + ident: syn::Ident, + /// The field name + name: String, + /// The field type + _type: String, +} + +impl FieldQuoter { + pub fn new(ident: syn::Ident, name: String, _type: String) -> FieldQuoter { + FieldQuoter { ident, name, _type } + } + + /// Don't put a & in front a pointer since we are going to pass + /// a reference to the validator + /// Also just use the ident without if it's optional and will go through + /// a if let first + pub fn quote_validator_param(&self) -> quote::Tokens { + let ident = &self.ident; + + if self._type.starts_with("Option<") { + quote!(#ident) + } else if self._type.starts_with("&") || NUMBER_TYPES.contains(&self._type.as_ref()) { + quote!(self.#ident) + } else { + quote!(&self.#ident) + } + } + + pub fn get_optional_validator_param(&self) -> quote::Tokens { + let ident = &self.ident; + if self._type.starts_with("Option<&") || NUMBER_TYPES.contains(&self._type.as_ref()) { + quote!(#ident) + } else { + quote!(ref #ident) + } + } + + /// Wrap the quoted output of a validation with a if let Some if + /// the field type is an option + pub fn wrap_if_option(&self, tokens: quote::Tokens) -> quote::Tokens { + let field_ident = &self.ident; + let optional_pattern_matched = self.get_optional_validator_param(); + if self._type.starts_with("Option<") { + return quote!( + if let Some(#optional_pattern_matched) = self.#field_ident { + #tokens + } + ) + } + + tokens + } +} + +/// Quote an actual end-user error creation automatically +fn quote_error(validation: &FieldValidation) -> quote::Tokens { + let code = &validation.code; + let add_message_quoted = if let Some(ref m) = validation.message { + quote!(err.message = Some(::std::borrow::Cow::from(#m));) + } else { + quote!() + }; + + quote!( + let mut err = ::validator::ValidationError::new(#code); + #add_message_quoted + ) +} + + +pub fn quote_length_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let field_name = &field_quoter.name; + let validator_param = field_quoter.quote_validator_param(); + + if let Validator::Length { min, max, equal } = validation.validator { + // Can't interpolate None + let min_tokens = option_u64_to_tokens(min); + let max_tokens = option_u64_to_tokens(max); + let equal_tokens = option_u64_to_tokens(equal); + + let min_err_param_quoted = if let Some(v) = min { + quote!(err.add_param(::std::borrow::Cow::from("min"), &#v);) + } else { + quote!() + }; + let max_err_param_quoted = if let Some(v) = max { + quote!(err.add_param(::std::borrow::Cow::from("max"), &#v);) + } else { + quote!() + }; + let equal_err_param_quoted = if let Some(v) = equal { + quote!(err.add_param(::std::borrow::Cow::from("equal"), &#v);) + } else { + quote!() + }; + + let quoted_error = quote_error(&validation); + let quoted = quote!( + if !::validator::validate_length( + ::validator::Validator::Length { + min: #min_tokens, + max: #max_tokens, + equal: #equal_tokens + }, + #validator_param + ) { + #quoted_error + #min_err_param_quoted + #max_err_param_quoted + #equal_err_param_quoted + err.add_param(::std::borrow::Cow::from("value"), &#validator_param); + errors.add(#field_name, err); + } + ); + + return field_quoter.wrap_if_option(quoted); + } + + unreachable!() +} + +pub fn quote_range_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let field_name = &field_quoter.name; + let quoted_ident = field_quoter.quote_validator_param(); + + if let Validator::Range { min, max } = validation.validator { + let quoted_error = quote_error(&validation); + let min_err_param_quoted = quote!(err.add_param(::std::borrow::Cow::from("min"), &#min);); + let max_err_param_quoted = quote!(err.add_param(::std::borrow::Cow::from("max"), &#max);); + let quoted = quote!( + if !::validator::validate_range( + ::validator::Validator::Range {min: #min, max: #max}, + #quoted_ident as f64 + ) { + #quoted_error + #min_err_param_quoted + #max_err_param_quoted + err.add_param(::std::borrow::Cow::from("value"), &#quoted_ident); + errors.add(#field_name, err); + } + ); + + return field_quoter.wrap_if_option(quoted); + } + + unreachable!() +} + +pub fn quote_url_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let field_name = &field_quoter.name; + let validator_param = field_quoter.quote_validator_param(); + + let quoted_error = quote_error(&validation); + let quoted = quote!( + if !::validator::validate_url(#validator_param) { + #quoted_error + err.add_param(::std::borrow::Cow::from("value"), &#validator_param); + errors.add(#field_name, err); + } + ); + + field_quoter.wrap_if_option(quoted) +} + +pub fn quote_email_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let field_name = &field_quoter.name; + let validator_param = field_quoter.quote_validator_param(); + + let quoted_error = quote_error(&validation); + let quoted = quote!( + if !::validator::validate_email(#validator_param) { + #quoted_error + err.add_param(::std::borrow::Cow::from("value"), &#validator_param); + errors.add(#field_name, err); + } + ); + + field_quoter.wrap_if_option(quoted) +} + +pub fn quote_must_match_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let ident = &field_quoter.ident; + let field_name = &field_quoter.name; + + if let Validator::MustMatch(ref other) = validation.validator { + let other_ident = syn::Ident::new(other.clone()); + let quoted_error = quote_error(&validation); + let quoted = quote!( + if !::validator::validate_must_match(&self.#ident, &self.#other_ident) { + #quoted_error + err.add_param(::std::borrow::Cow::from("value"), &self.#ident); + err.add_param(::std::borrow::Cow::from("other"), &self.#other_ident); + errors.add(#field_name, err); + } + ); + + return field_quoter.wrap_if_option(quoted); + } + + unreachable!(); +} + +pub fn quote_custom_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let field_name = &field_quoter.name; + let validator_param = field_quoter.quote_validator_param(); + + if let Validator::Custom(ref fun) = validation.validator { + let fn_ident = syn::Ident::new(fun.clone()); + let add_message_quoted = if let Some(ref m) = validation.message { + quote!(err.message = Some(::std::borrow::Cow::from(#m));) + } else { + quote!() + }; + + let quoted = quote!( + match #fn_ident(#validator_param) { + ::std::result::Result::Ok(()) => (), + ::std::result::Result::Err(mut err) => { + #add_message_quoted + err.add_param(::std::borrow::Cow::from("value"), &#validator_param); + errors.add(#field_name, err); + }, + }; + ); + + return field_quoter.wrap_if_option(quoted); + } + + unreachable!(); +} + +pub fn quote_contains_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let field_name = &field_quoter.name; + let validator_param = field_quoter.quote_validator_param(); + + if let Validator::Contains(ref needle) = validation.validator { + let quoted_error = quote_error(&validation); + let quoted = quote!( + if !::validator::validate_contains(#validator_param, &#needle) { + #quoted_error + err.add_param(::std::borrow::Cow::from("value"), &#validator_param); + err.add_param(::std::borrow::Cow::from("needle"), &#needle); + errors.add(#field_name, err); + } + ); + + return field_quoter.wrap_if_option(quoted); + } + + unreachable!(); +} + +pub fn quote_regex_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + let field_name = &field_quoter.name; + let validator_param = field_quoter.quote_validator_param(); + + if let Validator::Regex(ref re) = validation.validator { + let re_ident = syn::Ident::new(re.clone()); + let quoted_error = quote_error(&validation); + let quoted = quote!( + if !#re_ident.is_match(#validator_param) { + #quoted_error + err.add_param(::std::borrow::Cow::from("value"), &#validator_param); + errors.add(#field_name, err); + } + ); + + return field_quoter.wrap_if_option(quoted); + } + + unreachable!(); +} + +pub fn quote_field_validation(field_quoter: &FieldQuoter, validation: &FieldValidation) -> quote::Tokens { + 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), + } +} + + +pub fn quote_schema_validation(validation: Option<SchemaValidation>) -> quote::Tokens { + if let Some(v) = validation { + let fn_ident = syn::Ident::new(v.function); + + let add_message_quoted = if let Some(ref m) = v.message { + quote!(err.message = Some(::std::borrow::Cow::from(#m));) + } else { + quote!() + }; + let mut_err_token = if v.message.is_some() { quote!(mut) } else { quote!() }; + let quoted = quote!( + match #fn_ident(self) { + ::std::result::Result::Ok(()) => (), + ::std::result::Result::Err(#mut_err_token err) => { + #add_message_quoted + errors.add("__all__", err); + }, + }; + ); + + if !v.skip_on_field_errors { + return quoted; + } + + quote!( + if errors.is_empty() { + #quoted + } + ) + } else { + quote!() + } +} diff --git a/validator_derive/src/validation.rs b/validator_derive/src/validation.rs new file mode 100644 index 0000000..a3350da --- /dev/null +++ b/validator_derive/src/validation.rs @@ -0,0 +1,290 @@ +use syn; + +use validator::Validator; + +use lit::*; + + +#[derive(Debug)] +pub struct SchemaValidation { + pub function: String, + pub skip_on_field_errors: bool, + pub code: Option<String>, + pub message: Option<String>, +} + + +#[derive(Debug)] +pub struct FieldValidation { + pub code: String, + pub message: Option<String>, + pub validator: Validator, +} + +impl FieldValidation { + pub fn new(validator: Validator) -> FieldValidation { + FieldValidation { + code: validator.code().to_string(), + validator, + message: None, + } + } +} + +pub fn extract_length_validation(field: String, meta_items: &Vec<syn::NestedMetaItem>) -> FieldValidation { + let mut min = None; + let mut max = None; + let mut equal = None; + + let mut code = None; + let mut message = None; + + let error = |msg: &str| -> ! { + panic!("Invalid attribute #[validate] on field `{}`: {}", field, msg); + }; + + for meta_item in meta_items { + if let syn::NestedMetaItem::MetaItem(ref item) = *meta_item { + if let syn::MetaItem::NameValue(ref name, ref val) = *item { + match name.to_string().as_ref() { + "min" => { + min = match lit_to_int(val) { + Some(s) => Some(s), + None => error("invalid argument type for `min` of `length` validator: only integers are allowed"), + }; + }, + "max" => { + max = match lit_to_int(val) { + Some(s) => Some(s), + None => error("invalid argument type for `max` of `length` validator: only integers are allowed"), + }; + }, + "equal" => { + equal = match lit_to_int(val) { + Some(s) => Some(s), + None => error("invalid argument type for `equal` of `length` validator: only integers are allowed"), + }; + }, + "code" => { + code = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `code` of `length` validator: only a string is allowed"), + }; + }, + "message" => { + message = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `message` of `length` validator: only a string is allowed"), + }; + }, + _ => error(&format!( + "unknown argument `{}` for validator `length` (it only has `min`, `max`, `equal`)", + name.to_string() + )) + } + } else { + panic!("unexpected item {:?} while parsing `length` validator of field {}", item, field) + } + } + } + + if equal.is_some() && (min.is_some() || max.is_some()) { + error("both `equal` and `min` or `max` have been set in `length` validator: probably a mistake"); + } + if min.is_none() && max.is_none() && equal.is_none() { + error("Validator `length` requires at least 1 argument out of `min`, `max` and `equal`"); + } + + let validator = Validator::Length { min, max, equal }; + FieldValidation { + message, + code: code.unwrap_or_else(|| validator.code().to_string()), + validator, + } +} + +pub fn extract_range_validation(field: String, meta_items: &Vec<syn::NestedMetaItem>) -> FieldValidation { + let mut min = 0.0; + let mut max = 0.0; + + let mut code = None; + let mut message = None; + + let error = |msg: &str| -> ! { + panic!("Invalid attribute #[validate] on field `{}`: {}", field, msg); + }; + + // whether it has both `min` and `max` + let mut has_min = false; + let mut has_max = false; + + for meta_item in meta_items { + match *meta_item { + syn::NestedMetaItem::MetaItem(ref item) => match *item { + syn::MetaItem::NameValue(ref name, ref val) => { + match name.to_string().as_ref() { + "min" => { + min = match lit_to_float(val) { + Some(s) => s, + None => error("invalid argument type for `min` of `range` validator: only integers are allowed") + }; + has_min = true; + }, + "max" => { + max = match lit_to_float(val) { + Some(s) => s, + None => error("invalid argument type for `max` of `range` validator: only integers are allowed") + }; + has_max = true; + }, + "code" => { + code = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `code` of `length` validator: only a string is allowed"), + }; + }, + "message" => { + message = match lit_to_string(val) { + Some(s) => Some(s), + None => error("invalid argument type for `message` of `length` validator: only a string is allowed"), + }; + }, + _ => error(&format!( + "unknown argument `{}` for validator `range` (it only has `min`, `max`)", + name.to_string() + )) + } + }, + _ => panic!("unexpected item {:?} while parsing `range` validator", item) + }, + _=> unreachable!() + } + } + + if !has_min || !has_max { + error("Validator `range` requires 2 arguments: `min` and `max`"); + } + + let validator = Validator::Range { min, max }; + FieldValidation { + message, + code: code.unwrap_or_else(|| validator.code().to_string()), + validator, + } +} + +/// Extract url/email field validation with a code or a message +pub fn extract_argless_validation(validator_name: String, field: String, meta_items: &Vec<syn::NestedMetaItem>) -> FieldValidation { + let mut code = None; + let mut message = None; + + for meta_item in meta_items { + match *meta_item { + syn::NestedMetaItem::MetaItem(ref item) => match *item { + syn::MetaItem::NameValue(ref name, ref val) => { + match name.to_string().as_ref() { + "code" => { + code = match lit_to_string(val) { + Some(s) => Some(s), + None => panic!( + "Invalid argument type for `code` for validator `{}` on field `{}`: only a string is allowed", + validator_name, field + ), + }; + }, + "message" => { + message = match lit_to_string(val) { + Some(s) => Some(s), + None => panic!( + "Invalid argument type for `message` for validator `{}` on field `{}`: only a string is allowed", + validator_name, field + ), + }; + }, + _ => panic!( + "Unknown argument `{}` for validator `{}` on field `{}`", + name.to_string(), validator_name, field + ) + } + }, + _ => panic!("unexpected item {:?} while parsing `range` validator", item) + }, + _=> unreachable!() + } + } + + let validator = if validator_name == "email" { Validator::Email } else { Validator::Url }; + FieldValidation { + message, + code: code.unwrap_or_else(|| validator.code().to_string()), + validator, + } +} + +/// For custom, contains, regex, must_match +pub fn extract_one_arg_validation(val_name: &str, validator_name: String, field: String, meta_items: &Vec<syn::NestedMetaItem>) -> FieldValidation { + let mut code = None; + let mut message = None; + let mut value = None; + + for meta_item in meta_items { + match *meta_item { + syn::NestedMetaItem::MetaItem(ref item) => match *item { + syn::MetaItem::NameValue(ref name, ref val) => { + match name.to_string().as_ref() { + v if v == val_name => { + value = match lit_to_string(val) { + Some(s) => Some(s), + None => panic!( + "Invalid argument type for `{}` for validator `{}` on field `{}`: only a string is allowed", + val_name, validator_name, field + ), + }; + }, + "code" => { + code = match lit_to_string(val) { + Some(s) => Some(s), + None => panic!( + "Invalid argument type for `code` for validator `{}` on field `{}`: only a string is allowed", + validator_name, field + ), + }; + }, + "message" => { + message = match lit_to_string(val) { + Some(s) => Some(s), + None => panic!( + "Invalid argument type for `message` for validator `{}` on field `{}`: only a string is allowed", + validator_name, field + ), + }; + }, + _ => panic!( + "Unknown argument `{}` for validator `{}` on field `{}`", + name.to_string(), validator_name, field + ) + } + }, + _ => panic!("unexpected item {:?} while parsing `range` validator", item) + }, + _=> unreachable!() + } + } + + if value.is_none() { + panic!("Missing argument `{}` for validator `{}` on field `{}`", val_name, validator_name, field); + } + + let validator = match validator_name.as_ref() { + "custom" => Validator::Custom(value.unwrap()), + "contains" => Validator::Contains(value.unwrap()), + "must_match" => Validator::MustMatch(value.unwrap()), + "regex" => Validator::Regex(value.unwrap()), + _ => unreachable!(), + }; + FieldValidation { + message, + code: code.unwrap_or_else(|| validator.code().to_string()), + validator, + } +} diff --git a/validator_derive/tests/compile-fail/length/wrong_type.rs b/validator_derive/tests/compile-fail/length/wrong_type.rs index 365675f..a9d53d4 100644 --- a/validator_derive/tests/compile-fail/length/wrong_type.rs +++ b/validator_derive/tests/compile-fail/length/wrong_type.rs @@ -4,7 +4,7 @@ use validator::Validate; #[derive(Validate)] //~^ ERROR: proc-macro derive panicked -//~^^ HELP: Invalid attribute #[validate] on field `s`: Validator `length` can only be used on types `String`, `&str` or `Vec` but found `usize` +//~^^ HELP: Validator `length` can only be used on types `String`, `&str` or `Vec` but found `usize` struct Test { #[validate(length())] s: usize, diff --git a/validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs b/validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs index f3a2347..1ba5319 100644 --- a/validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs +++ b/validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs @@ -4,7 +4,7 @@ use validator::Validate; #[derive(Validate)] //~^ ERROR: proc-macro derive panicked -//~^^ HELP: Invalid attribute #[validate] on field `password`: invalid argument for `must_match` validator: field doesn't exist in struct +//~^^ HELP: Invalid argument for `must_match` validator of field `password`: the other field doesn't exist in struct struct Test { #[validate(must_match = "password2")] password: String, diff --git a/validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs b/validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs index b04a732..580495a 100644 --- a/validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs +++ b/validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs @@ -4,7 +4,7 @@ use validator::Validate; #[derive(Validate)] //~^ ERROR: proc-macro derive panicked -//~^^ HELP: Invalid attribute #[validate] on field `password`: invalid argument for `must_match` validator: types of field can't match +//~^^ HELP: Invalid argument for `must_match` validator of field `password`: types of field can't match struct Test { #[validate(must_match = "password2")] password: String, diff --git a/validator_derive/tests/compile-fail/range/wrong_type.rs b/validator_derive/tests/compile-fail/range/wrong_type.rs index 239a93d..f6a0a43 100644 --- a/validator_derive/tests/compile-fail/range/wrong_type.rs +++ b/validator_derive/tests/compile-fail/range/wrong_type.rs @@ -6,7 +6,7 @@ use validator::Validate; #[derive(Validate)] //~^ ERROR: proc-macro derive panicked -//~^^ HELP: Invalid attribute #[validate] on field `s`: Validator `range` can only be used on number types but found `String` +//~^^ HELP: Validator `range` can only be used on number types but found `String` struct Test { #[validate(range(min = 10.0, max = 12.0))] s: String, diff --git a/validator_derive/tests/complex.rs b/validator_derive/tests/complex.rs new file mode 100644 index 0000000..db5f1bf --- /dev/null +++ b/validator_derive/tests/complex.rs @@ -0,0 +1,155 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; +extern crate regex; +#[macro_use] +extern crate lazy_static; + +use regex::Regex; +use validator::{Validate, ValidationError}; + + +fn validate_unique_username(username: &str) -> Result<(), ValidationError> { + if username == "xXxShad0wxXx" { + return Err(ValidationError::new("terrible_username")); + } + + Ok(()) +} + +fn validate_signup(data: &SignupData) -> Result<(), ValidationError> { + if data.mail.ends_with("gmail.com") && data.age == 18 { + return Err(ValidationError::new("stupid_rule")); + } + + Ok(()) +} + +#[derive(Debug, Validate, Deserialize)] +#[validate(schema(function = "validate_signup", skip_on_field_errors = "false"))] +struct SignupData { + #[validate(email)] + mail: String, + #[validate(url)] + site: String, + #[validate(length(min = "1"), custom = "validate_unique_username")] + #[serde(rename = "firstName")] + first_name: String, + #[validate(range(min = "18", max = "20"))] + age: u32, +} + + +#[test] +fn is_fine_with_many_valid_validations() { + let signup = SignupData { + mail: "bob@bob.com".to_string(), + site: "http://hello.com".to_string(), + first_name: "Bob".to_string(), + age: 18, + }; + + assert!(signup.validate().is_ok()); +} + +#[test] +fn failed_validation_points_to_original_field_name() { + let signup = SignupData { + mail: "bob@bob.com".to_string(), + site: "http://hello.com".to_string(), + first_name: "".to_string(), + age: 18, + }; + let res = signup.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("firstName")); + assert_eq!(errs["firstName"].len(), 1); + assert_eq!(errs["firstName"][0].code, "length"); +} + +#[test] +fn test_can_validate_option_fields_with_lifetime() { + lazy_static! { + static ref RE2: Regex = Regex::new(r"[a-z]{2}").unwrap(); + } + + #[derive(Debug, Validate)] + struct PutStruct<'a> { + #[validate(length(min = "1", max = "10"))] + name: Option<&'a str>, + #[validate(range(min = "1", max = "10"))] + range: Option<usize>, + #[validate(email)] + email: Option<&'a str>, + #[validate(url)] + url: Option<&'a str>, + #[validate(contains = "@")] + text: Option<&'a str>, + #[validate(regex = "RE2")] + re: Option<&'a str>, + #[validate(custom = "check_str")] + custom: Option<&'a str>, + } + + fn check_str(_: &str) -> Result<(), ValidationError> { + Ok(()) + } + + let s = PutStruct { + name: Some("al"), + range: Some(2), + email: Some("hi@gmail.com"), + url: Some("http://google.com"), + text: Some("@someone"), + re: Some("hi"), + custom: Some("hey"), + }; + assert!(s.validate().is_ok()); +} + +#[test] +fn test_can_validate_option_fields_without_lifetime() { + lazy_static! { + static ref RE2: Regex = Regex::new(r"[a-z]{2}").unwrap(); + } + + #[derive(Debug, Validate)] + struct PutStruct { + #[validate(length(min = "1", max = "10"))] + name: Option<String>, + #[validate(length(min = "1", max = "10"))] + ids: Option<Vec<usize>>, + #[validate(range(min = "1", max = "10"))] + range: Option<usize>, + #[validate(email)] + email: Option<String>, + #[validate(url)] + url: Option<String>, + #[validate(contains = "@")] + text: Option<String>, + #[validate(regex = "RE2")] + re: Option<String>, + #[validate(custom = "check_str")] + custom: Option<String>, + } + + fn check_str(_: &str) -> Result<(), ValidationError> { + Ok(()) + } + + let s = PutStruct { + name: Some("al".to_string()), + ids: Some(vec![1, 2, 3]), + range: Some(2), + email: Some("hi@gmail.com".to_string()), + url: Some("http://google.com".to_string()), + text: Some("@someone".to_string()), + re: Some("hi".to_string()), + custom: Some("hey".to_string()), + }; + assert!(s.validate().is_ok()); +} diff --git a/validator_derive/tests/contains.rs b/validator_derive/tests/contains.rs new file mode 100644 index 0000000..847de0f --- /dev/null +++ b/validator_derive/tests/contains.rs @@ -0,0 +1,77 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; + +#[test] +fn can_validate_contains_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(contains = "he")] + val: String, + } + + let s = TestStruct { + val: "hello".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn value_not_containing_needle_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(contains = "he")] + val: String, + } + + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "contains"); + assert_eq!(errs["val"][0].params["value"], ""); + assert_eq!(errs["val"][0].params["needle"], "he"); +} + +#[test] +fn can_specify_code_for_contains() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(contains(pattern = "he", code = "oops"))] + val: String, + } + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_message_for_contains() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(contains(pattern = "he", message = "oops"))] + val: String, + } + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + 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 new file mode 100644 index 0000000..0cac75f --- /dev/null +++ b/validator_derive/tests/custom.rs @@ -0,0 +1,66 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::{Validate, ValidationError}; + +fn valid_custom_fn(_: &str) -> Result<(), ValidationError> { + Ok(()) +} + +fn invalid_custom_fn(_: &str) -> Result<(), ValidationError> { + Err(ValidationError::new("meh")) +} + +#[test] +fn can_validate_custom_fn_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(custom = "valid_custom_fn")] + val: String, + } + + let s = TestStruct { + val: "hello".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn can_fail_custom_fn_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(custom = "invalid_custom_fn")] + val: String, + } + + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "meh"); + assert_eq!(errs["val"][0].params["value"], ""); +} + +#[test] +fn can_specify_message_for_custom_fn() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(custom(function = "invalid_custom_fn", message = "oops"))] + val: String, + } + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + 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 new file mode 100644 index 0000000..fc05bf9 --- /dev/null +++ b/validator_derive/tests/email.rs @@ -0,0 +1,77 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; + + +#[test] +fn can_validate_valid_email() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(email)] + val: String, + } + + let s = TestStruct { + val: "bob@bob.com".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn bad_email_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(email)] + val: String, + } + + let s = TestStruct { + val: "bob".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "email"); + assert_eq!(errs["val"][0].params["value"], "bob"); +} + +#[test] +fn can_specify_code_for_email() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(email(code = "oops"))] + val: String, + } + let s = TestStruct { + val: "bob".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_message_for_email() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(email(message = "oops"))] + val: String, + } + let s = TestStruct { + val: "bob".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + 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 new file mode 100644 index 0000000..892cf74 --- /dev/null +++ b/validator_derive/tests/length.rs @@ -0,0 +1,78 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; + +#[test] +fn can_validate_length_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(length(min = "5", max = "10"))] + val: String, + } + + let s = TestStruct { + val: "hello".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn value_out_of_length_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(length(min = "5", max = "10"))] + val: String, + } + + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "length"); + assert_eq!(errs["val"][0].params["value"], ""); + assert_eq!(errs["val"][0].params["min"], 5); + assert_eq!(errs["val"][0].params["max"], 10); +} + +#[test] +fn can_specify_code_for_length() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(length(min = "5", max = "10", code = "oops"))] + val: String, + } + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_message_for_length() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(length(min = "5", max = "10", message = "oops"))] + val: String, + } + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + 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 new file mode 100644 index 0000000..038616a --- /dev/null +++ b/validator_derive/tests/must_match.rs @@ -0,0 +1,87 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; + + +#[test] +fn can_validate_valid_must_match() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_match = "val2")] + val: String, + val2: String, + } + + let s = TestStruct { + val: "bob".to_string(), + val2: "bob".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn not_matching_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_match = "val2")] + val: String, + val2: String, + } + + let s = TestStruct { + val: "bob".to_string(), + val2: "bobby".to_string(), + }; + + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "must_match"); + assert_eq!(errs["val"][0].params["value"], "bob"); + assert_eq!(errs["val"][0].params["other"], "bobby"); +} + +#[test] +fn can_specify_code_for_must_match() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_match(other = "val2", code = "oops"))] + val: String, + val2: String, + } + let s = TestStruct { + val: "bob".to_string(), + val2: "bobb".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_message_for_must_match() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(must_match(other = "val2", message = "oops"))] + val: String, + val2: String, + } + let s = TestStruct { + val: "bob".to_string(), + val2: "bobb".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + 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 new file mode 100644 index 0000000..2ecd678 --- /dev/null +++ b/validator_derive/tests/range.rs @@ -0,0 +1,78 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; + +#[test] +fn can_validate_range_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(min = "5", max = "10"))] + val: usize, + } + + let s = TestStruct { + val: 6, + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn value_out_of_range_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(min = "5", max = "10"))] + val: usize, + } + + let s = TestStruct { + val: 11, + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "range"); +} + +#[test] +fn can_specify_code_for_range() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(min = "5", max = "10", code = "oops"))] + val: usize, + } + let s = TestStruct { + val: 11, + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); + assert_eq!(errs["val"][0].params["value"], 11); + assert_eq!(errs["val"][0].params["min"], 5f64); + assert_eq!(errs["val"][0].params["max"], 10f64); +} + +#[test] +fn can_specify_message_for_range() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(range(min = "5", max = "10", message = "oops"))] + val: usize, + } + let s = TestStruct { + val: 1, + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + 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 new file mode 100644 index 0000000..58f8f69 --- /dev/null +++ b/validator_derive/tests/regex.rs @@ -0,0 +1,84 @@ +extern crate regex; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; +use regex::Regex; + +lazy_static! { + static ref RE2: Regex = Regex::new(r"^[a-z]{2}$").unwrap(); +} + +#[test] +fn can_validate_valid_regex() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(regex = "RE2")] + val: String, + } + + let s = TestStruct { + val: "aa".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn bad_value_for_regex_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(regex = "RE2")] + val: String, + } + + let s = TestStruct { + val: "2".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "regex"); + assert_eq!(errs["val"][0].params["value"], "2"); +} + +#[test] +fn can_specify_code_for_regex() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(regex(path = "RE2", code = "oops"))] + val: String, + } + let s = TestStruct { + val: "2".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_message_for_regex() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(regex(path = "RE2", message = "oops"))] + val: String, + } + let s = TestStruct { + val: "2".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + 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/run-pass/custom.rs b/validator_derive/tests/run-pass/custom.rs index 642b2d2..3a2a396 100644 --- a/validator_derive/tests/run-pass/custom.rs +++ b/validator_derive/tests/run-pass/custom.rs @@ -2,7 +2,7 @@ #[macro_use] extern crate validator_derive; extern crate validator; -use validator::Validate; +use validator::{Validate, ValidationError}; #[derive(Validate)] struct Test { @@ -10,8 +10,8 @@ struct Test { s: String, } -fn validate_something(s: &str) -> Option<String> { - Some(s.to_string()) +fn validate_something(s: &str) -> Result<(), ValidationError> { + Ok(()) } fn main() {} diff --git a/validator_derive/tests/run-pass/schema.rs b/validator_derive/tests/run-pass/schema.rs index 788d1e2..e219698 100644 --- a/validator_derive/tests/run-pass/schema.rs +++ b/validator_derive/tests/run-pass/schema.rs @@ -2,7 +2,7 @@ #[macro_use] extern crate validator_derive; extern crate validator; -use validator::Validate; +use validator::{Validate, ValidationError}; #[derive(Validate)] #[validate(schema(function = "hey"))] @@ -10,8 +10,8 @@ struct Test { s: String, } -fn hey(_: &Test) -> Option<(String, String)> { - None +fn hey(_: &Test) -> Result<(), ValidationError> { + Ok(()) } #[derive(Validate)] @@ -20,8 +20,9 @@ struct Test2 { s: String, } -fn hey2(_: &Test2) -> Option<(String, String)> { - None +fn hey2(_: &Test2) -> Result<(), ValidationError> { + Ok(()) } + fn main() {} diff --git a/validator_derive/tests/schema.rs b/validator_derive/tests/schema.rs new file mode 100644 index 0000000..6b3ae4f --- /dev/null +++ b/validator_derive/tests/schema.rs @@ -0,0 +1,99 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::{Validate, ValidationError}; + + +#[test] +fn can_validate_schema_fn_ok() { + fn valid_schema_fn(_: &TestStruct) -> Result<(), ValidationError> { + Ok(()) +} + + #[derive(Debug, Validate)] + #[validate(schema(function = "valid_schema_fn"))] + struct TestStruct { + val: String, + } + + let s = TestStruct { + val: "hello".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn can_fail_schema_fn_validation() { + fn invalid_schema_fn(_: &TestStruct) -> Result<(), ValidationError> { + Err(ValidationError::new("meh")) + } + + #[derive(Debug, Validate)] + #[validate(schema(function = "invalid_schema_fn"))] + struct TestStruct { + val: String, + } + + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("__all__")); + assert_eq!(errs["__all__"].len(), 1); + assert_eq!(errs["__all__"][0].code, "meh"); +} + +#[test] +fn can_specify_message_for_schema_fn() { + fn invalid_schema_fn(_: &TestStruct) -> Result<(), ValidationError> { + Err(ValidationError::new("meh")) + } + + #[derive(Debug, Validate)] + #[validate(schema(function = "invalid_schema_fn", message = "oops"))] + struct TestStruct { + val: String, + } + let s = TestStruct { + val: String::new(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("__all__")); + assert_eq!(errs["__all__"].len(), 1); + assert_eq!(errs["__all__"][0].clone().message.unwrap(), "oops"); +} + +#[test] +fn can_choose_to_run_schema_validation_even_after_field_errors() { + fn invalid_schema_fn(_: &TestStruct) -> Result<(), ValidationError> { + Err(ValidationError::new("meh")) + } + #[derive(Debug, Validate)] + #[validate(schema(function = "invalid_schema_fn", skip_on_field_errors = "false"))] + struct TestStruct { + val: String, + #[validate(range(min = "1", max = "10"))] + num: usize, + } + + let s = TestStruct { + val: "hello".to_string(), + num: 0, + }; + + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("__all__")); + assert_eq!(errs["__all__"].len(), 1); + assert_eq!(errs["__all__"][0].clone().code, "meh"); + assert!(errs.contains_key("num")); + assert_eq!(errs["num"].len(), 1); + assert_eq!(errs["num"][0].clone().code, "range"); +} diff --git a/validator_derive/tests/test_derive.rs b/validator_derive/tests/test_derive.rs deleted file mode 100644 index 24c408a..0000000 --- a/validator_derive/tests/test_derive.rs +++ /dev/null @@ -1,357 +0,0 @@ -#[macro_use] extern crate validator_derive; -extern crate validator; -#[macro_use] extern crate serde_derive; -extern crate serde_json; -extern crate regex; -#[macro_use] extern crate lazy_static; - -use validator::Validate; -use regex::Regex; - - -#[derive(Debug, Validate, Deserialize)] -#[validate(schema(function = "validate_signup", skip_on_field_errors = "false"))] -struct SignupData { - #[validate(email)] - mail: String, - #[validate(url)] - site: String, - #[validate(length(min = "1"), custom = "validate_unique_username")] - #[serde(rename = "firstName")] - first_name: String, - #[validate(range(min = "18", max = "20"))] - age: u32, -} - -#[derive(Debug, Validate)] -struct PasswordData { - #[validate(must_match = "password2")] - password: String, - password2: String, -} - - -fn validate_unique_username(username: &str) -> Option<String> { - if username == "xXxShad0wxXx" { - return Some("terrible_username".to_string()); - } - - None -} - -fn validate_signup(data: &SignupData) -> Option<(String, String)> { - if data.mail.ends_with("gmail.com") && data.age == 18 { - return Some(("all".to_string(), "stupid_rule".to_string())); - } - - None -} - -#[derive(Debug, Validate, Deserialize)] -#[validate(schema(function = "validate_signup2", skip_on_field_errors = "false"))] -struct SignupData2 { - #[validate(email)] - mail: String, - #[validate(range(min = "18", max = "20"))] - age: u32, -} - -#[derive(Debug, Validate, Deserialize)] -#[validate(schema(function = "validate_signup3"))] -struct SignupData3 { - #[validate(email, contains = "bob")] - mail: String, - #[validate(range(min = "18", max = "20"))] - age: u32, -} - -fn validate_signup2(data: &SignupData2) -> Option<(String, String)> { - if data.mail.starts_with("bob") && data.age == 18 { - return Some(("mail".to_string(), "stupid_rule".to_string())); - } - - None -} - -fn validate_signup3(_: &SignupData3) -> Option<(String, String)> { - Some(("mail".to_string(), "stupid_rule".to_string())) -} - -#[test] -fn test_can_validate_ok() { - let signup = SignupData { - mail: "bob@bob.com".to_string(), - site: "http://hello.com".to_string(), - first_name: "Bob".to_string(), - age: 18, - }; - - assert!(signup.validate().is_ok()); -} - -#[test] -fn test_bad_email_fails_validation() { - let signup = SignupData { - mail: "bob".to_string(), - site: "http://hello.com".to_string(), - first_name: "Bob".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("mail")); - assert_eq!(errs["mail"], vec!["email".to_string()]); -} - -#[test] -fn test_bad_url_fails_validation() { - let signup = SignupData { - mail: "bob@bob.com".to_string(), - site: "//hello.com".to_string(), - first_name: "Bob".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("site")); - assert_eq!(errs["site"], vec!["url".to_string()]); -} - -#[test] -fn test_bad_length_fails_validation_and_points_to_original_name() { - let signup = SignupData { - mail: "bob@bob.com".to_string(), - site: "http://hello.com".to_string(), - first_name: "".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("firstName")); - assert_eq!(errs["firstName"], vec!["length".to_string()]); -} - - -#[test] -fn test_bad_range_fails_validation() { - let signup = SignupData { - mail: "bob@bob.com".to_string(), - site: "https://hello.com".to_string(), - first_name: "Bob".to_string(), - age: 1, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("age")); - assert_eq!(errs["age"], vec!["range".to_string()]); -} - -#[test] -fn test_can_have_multiple_errors() { - let signup = SignupData { - mail: "bob@bob.com".to_string(), - site: "https://hello.com".to_string(), - first_name: "".to_string(), - age: 1, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("age")); - assert!(errs.contains_key("firstName")); - assert_eq!(errs["age"], vec!["range".to_string()]); - assert_eq!(errs["firstName"], vec!["length".to_string()]); -} - -#[test] -fn test_custom_validation_error() { - let signup = SignupData { - mail: "bob@bob.com".to_string(), - site: "https://hello.com".to_string(), - first_name: "xXxShad0wxXx".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("firstName")); - assert_eq!(errs["firstName"], vec!["terrible_username".to_string()]); -} - -#[test] -fn test_must_match_can_work() { - let data = PasswordData { - password: "passw0rd".to_string(), - password2: "passw0rd".to_string(), - }; - assert!(data.validate().is_ok()) -} - - -#[test] -fn test_must_match_can_fail() { - let data = PasswordData { - password: "passw0rd".to_string(), - password2: "password".to_string(), - }; - assert!(data.validate().is_err()) -} - -#[test] -fn test_can_fail_struct_validation_new_key() { - let signup = SignupData { - mail: "bob@gmail.com".to_string(), - site: "https://hello.com".to_string(), - first_name: "xXxShad0wxXx".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("all")); - assert_eq!(errs["all"], vec!["stupid_rule".to_string()]); -} - -#[test] -fn test_can_fail_struct_validation_existing_key() { - let signup = SignupData2 { - mail: "bob".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("mail")); - assert_eq!(errs["mail"], vec!["email".to_string(), "stupid_rule".to_string()]); -} - -#[test] -fn test_skip_struct_validation_by_default_if_errors() { - let signup = SignupData3 { - mail: "bob".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("mail")); - assert_eq!(errs["mail"], vec!["email".to_string()]); -} - -#[test] -fn test_can_fail_contains_validation() { - let signup = SignupData3 { - mail: "bo@gmail.com".to_string(), - age: 18, - }; - let res = signup.validate(); - assert!(res.is_err()); - let errs = res.unwrap_err().inner(); - assert!(errs.contains_key("mail")); - assert_eq!(errs["mail"], vec!["contains".to_string()]); -} - -#[test] -fn test_can_check_regex_validator() { - lazy_static! { - static ref RE: Regex = Regex::new(r"[a-z]{2}").unwrap(); - } - - #[derive(Debug, Validate)] - struct RegexStruct { - #[validate(regex = "RE")] - name: String, - } - let s = RegexStruct {name: "al".to_string()}; - assert!(s.validate().is_ok()); - let s2 = RegexStruct {name: "AL".to_string()}; - assert!(s2.validate().is_err()); -} - - -#[test] -fn test_can_validate_option_fields_with_lifetime() { - lazy_static! { - static ref RE2: Regex = Regex::new(r"[a-z]{2}").unwrap(); - } - - #[derive(Debug, Validate)] - struct PutStruct<'a> { - #[validate(length(min = "1", max = "10"))] - name: Option<&'a str>, - #[validate(range(min = "1", max = "10"))] - range: Option<usize>, - #[validate(email)] - email: Option<&'a str>, - #[validate(url)] - url: Option<&'a str>, - #[validate(contains = "@")] - text: Option<&'a str>, - #[validate(regex = "RE2")] - re: Option<&'a str>, - #[validate(custom = "check_str")] - custom: Option<&'a str>, - } - - fn check_str(_: &str) -> Option<String> { - None - } - - let s = PutStruct { - name: Some("al"), - range: Some(2), - email: Some("hi@gmail.com"), - url: Some("http://google.com"), - text: Some("@someone"), - re: Some("hi"), - custom: Some("hey"), - }; - assert!(s.validate().is_ok()); -} - -#[test] -fn test_can_validate_option_fields_without_lifetime() { - lazy_static! { - static ref RE2: Regex = Regex::new(r"[a-z]{2}").unwrap(); - } - - #[derive(Debug, Validate)] - struct PutStruct { - #[validate(length(min = "1", max = "10"))] - name: Option<String>, - #[validate(length(min = "1", max = "10"))] - ids: Option<Vec<usize>>, - #[validate(range(min = "1", max = "10"))] - range: Option<usize>, - #[validate(email)] - email: Option<String>, - #[validate(url)] - url: Option<String>, - #[validate(contains = "@")] - text: Option<String>, - #[validate(regex = "RE2")] - re: Option<String>, - #[validate(custom = "check_str")] - custom: Option<String>, - } - - fn check_str(_: &str) -> Option<String> { - None - } - - let s = PutStruct { - name: Some("al".to_string()), - ids: Some(vec![1, 2, 3]), - range: Some(2), - email: Some("hi@gmail.com".to_string()), - url: Some("http://google.com".to_string()), - text: Some("@someone".to_string()), - re: Some("hi".to_string()), - custom: Some("hey".to_string()), - }; - assert!(s.validate().is_ok()); -} diff --git a/validator_derive/tests/url.rs b/validator_derive/tests/url.rs new file mode 100644 index 0000000..1182086 --- /dev/null +++ b/validator_derive/tests/url.rs @@ -0,0 +1,77 @@ +#[macro_use] +extern crate validator_derive; +extern crate validator; + +use validator::Validate; + + +#[test] +fn can_validate_url_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(url)] + val: String, + } + + let s = TestStruct { + val: "https://google.com".to_string(), + }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn bad_url_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(url)] + val: String, + } + + let s = TestStruct { + val: "bob".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "url"); +} + +#[test] +fn can_specify_code_for_url() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(url(code = "oops"))] + val: String, + } + let s = TestStruct { + val: "bob".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); + assert_eq!(errs["val"][0].params["value"], "bob"); +} + +#[test] +fn can_specify_message_for_url() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(url(message = "oops"))] + val: String, + } + let s = TestStruct { + val: "bob".to_string(), + }; + let res = s.validate(); + assert!(res.is_err()); + let errs = res.unwrap_err().inner(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); +} |
