aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVincent Prouillet2017-07-19 16:30:01 +0900
committerGitHub2017-07-19 16:30:01 +0900
commit2ab1a0fa0fe8fe9ccc025cbd7228e6b8201b1f1f (patch)
treee71b13aea39c61b19e8aec09468a8e5c4a291e16
parent69e35d6cc905b9d7a894af7e486237e376fae939 (diff)
parentb31fd25cc5cec96ccee737ba1313c6e9f702f32a (diff)
downloadvalidator-2ab1a0fa0fe8fe9ccc025cbd7228e6b8201b1f1f.tar.bz2
Merge pull request #27 from Keats/revamp
Complete refactor
-rw-r--r--validator/Cargo.toml3
-rw-r--r--validator/src/lib.rs35
-rw-r--r--validator/src/traits.rs82
-rw-r--r--validator/src/types.rs104
-rw-r--r--validator/src/validation/contains.rs (renamed from validator/src/contains.rs)39
-rw-r--r--validator/src/validation/email.rs (renamed from validator/src/email.rs)4
-rw-r--r--validator/src/validation/ip.rs (renamed from validator/src/ip.rs)0
-rw-r--r--validator/src/validation/length.rs (renamed from validator/src/length.rs)40
-rw-r--r--validator/src/validation/mod.rs50
-rw-r--r--validator/src/validation/must_match.rs (renamed from validator/src/must_match.rs)0
-rw-r--r--validator/src/validation/range.rs (renamed from validator/src/range.rs)2
-rw-r--r--validator/src/validation/urls.rs (renamed from validator/src/urls.rs)0
-rw-r--r--validator_derive/Cargo.toml5
-rw-r--r--validator_derive/src/asserts.rs54
-rw-r--r--validator_derive/src/lib.rs672
-rw-r--r--validator_derive/src/lit.rs61
-rw-r--r--validator_derive/src/quoting.rs332
-rw-r--r--validator_derive/src/validation.rs290
-rw-r--r--validator_derive/tests/compile-fail/length/wrong_type.rs2
-rw-r--r--validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs2
-rw-r--r--validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs2
-rw-r--r--validator_derive/tests/compile-fail/range/wrong_type.rs2
-rw-r--r--validator_derive/tests/complex.rs155
-rw-r--r--validator_derive/tests/contains.rs77
-rw-r--r--validator_derive/tests/custom.rs66
-rw-r--r--validator_derive/tests/email.rs77
-rw-r--r--validator_derive/tests/length.rs78
-rw-r--r--validator_derive/tests/must_match.rs87
-rw-r--r--validator_derive/tests/range.rs78
-rw-r--r--validator_derive/tests/regex.rs84
-rw-r--r--validator_derive/tests/run-pass/custom.rs6
-rw-r--r--validator_derive/tests/run-pass/schema.rs11
-rw-r--r--validator_derive/tests/schema.rs99
-rw-r--r--validator_derive/tests/test_derive.rs357
-rw-r--r--validator_derive/tests/url.rs77
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");
+}