diff options
| -rw-r--r-- | README.md | 10 | ||||
| -rw-r--r-- | validator/src/contains.rs | 70 | ||||
| -rw-r--r-- | validator/src/length.rs | 6 | ||||
| -rw-r--r-- | validator/src/lib.rs | 2 | ||||
| -rw-r--r-- | validator/src/types.rs | 29 | ||||
| -rw-r--r-- | validator_derive/Cargo.toml | 4 | ||||
| -rw-r--r-- | validator_derive/src/lib.rs | 35 | ||||
| -rw-r--r-- | validator_derive/tests/test_derive.rs | 33 |
8 files changed, 161 insertions, 28 deletions
@@ -112,6 +112,16 @@ Examples: #[validate(must_match = "password2"))] ``` +### contains +Tests whether the string contains the substring given or if a key is present in a hashmap. `contains` takes +1 string argument. + +Examples: + +```rust +#[validate(contains = "gmail"))] +``` + ### custom Calls one of your function to do a custom validation. The field will be given as parameter and it should return a `Option<String>` representing the error code, diff --git a/validator/src/contains.rs b/validator/src/contains.rs new file mode 100644 index 0000000..89f7523 --- /dev/null +++ b/validator/src/contains.rs @@ -0,0 +1,70 @@ +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<'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) + } +} + +/// Validates whether the value contains the needle +/// The value needs to implement the Contains trait, which is implement on String, str and Hashmap<String> +/// by default. +pub fn validate_contains<T: Contains>(val: T, needle: &str) -> bool { + val.has_element(needle) +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use super::*; + + #[test] + fn test_validate_contains_string() { + assert!(validate_contains("hey", "e")); + } + + #[test] + fn test_validate_contains_string_can_fail() { + assert_eq!(validate_contains("hey", "o"), false); + } + + #[test] + fn test_validate_contains_hashmap_key() { + let mut map = HashMap::new(); + map.insert("hey".to_string(), 1); + assert!(validate_contains(map, "hey")); + } + + #[test] + fn test_validate_contains_hashmap_key_can_fail() { + let mut map = HashMap::new(); + map.insert("hey".to_string(), 1); + assert_eq!(validate_contains(map, "bob"), false); + } +} diff --git a/validator/src/length.rs b/validator/src/length.rs index 358317f..f54a002 100644 --- a/validator/src/length.rs +++ b/validator/src/length.rs @@ -1,7 +1,9 @@ use types::Validator; -// a bit sad but we can generically refer to a struct that has a len() method -// so we impl our own trait for it +/// 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; } diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 3c52661..2e44b2e 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -11,6 +11,7 @@ mod length; mod range; mod urls; mod must_match; +mod contains; pub use types::{Errors, Validate, Validator}; @@ -20,3 +21,4 @@ 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}; diff --git a/validator/src/types.rs b/validator/src/types.rs index 67e8245..d91a25e 100644 --- a/validator/src/types.rs +++ b/validator/src/types.rs @@ -1,13 +1,34 @@ use std::collections::HashMap; -pub type Errors = HashMap<String, Vec<String>>; +pub struct Errors(HashMap<String, Vec<String>>); + +impl Errors { + pub fn new() -> Errors { + Errors(HashMap::new()) + } + + pub fn inner(self) -> HashMap<String, Vec<String>> { + self.0 + } + + pub fn add(&mut self, field: &str, err: &str) { + self.0.entry(field.to_string()).or_insert_with(|| vec![]).push(err.to_string()); + } + + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } +} pub trait Validate { - //fn load_and_validate<T>(data: &str) -> Result<T, Errors>; fn validate(&self) -> Result<(), Errors>; } +/// 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 @@ -18,6 +39,10 @@ pub enum Validator { 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, diff --git a/validator_derive/Cargo.toml b/validator_derive/Cargo.toml index da556a7..e5234f9 100644 --- a/validator_derive/Cargo.toml +++ b/validator_derive/Cargo.toml @@ -22,5 +22,5 @@ serde_json = "0.8" compiletest_rs = "0.2" [dependencies.validator] -# path = "../validator" -version = "0.2.0" +path = "../validator" +# version = "0.2.0" diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index 9725ab6..7a21ac8 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -75,7 +75,7 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { }, &self.#field_ident ) { - errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("length".to_string()); + errors.add(#name, "length"); } ) }, @@ -85,21 +85,21 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { ::validator::Validator::Range {min: #min, max: #max}, self.#field_ident as f64 ) { - errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("range".to_string()); + errors.add(#name, "range"); } ) }, &Validator::Email => { quote!( if !::validator::validate_email(&self.#field_ident) { - errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("email".to_string()); + errors.add(#name, "email"); } ) } &Validator::Url => { quote!( if !::validator::validate_url(&self.#field_ident) { - errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("url".to_string()); + errors.add(#name, "url"); } ) }, @@ -107,7 +107,7 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { let other_ident = syn::Ident::new(f.clone()); quote!( if !::validator::validate_must_match(&self.#field_ident, &self.#other_ident) { - errors.entry(#name.to_string()).or_insert_with(|| vec![]).push("no_match".to_string()); + errors.add(#name, "no_match"); } ) }, @@ -116,12 +116,19 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { quote!( match #fn_ident(&self.#field_ident) { ::std::option::Option::Some(s) => { - errors.entry(#name.to_string()).or_insert_with(|| vec![]).push(s) + errors.add(#name, &s); }, ::std::option::Option::None => (), }; ) }, + &Validator::Contains(ref n) => { + quote!( + if !::validator::validate_contains(&self.#field_ident, &#n) { + errors.add(#name, "contains"); + } + ) + }, }); } } @@ -135,7 +142,7 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { if errors.is_empty() { match #fn_ident(self) { ::std::option::Option::Some((key, val)) => { - errors.entry(key).or_insert_with(|| vec![]).push(val) + errors.add(&key, &val); }, ::std::option::Option::None => (), } @@ -145,7 +152,7 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { quote!( match #fn_ident(self) { ::std::option::Option::Some((key, val)) => { - errors.entry(key).or_insert_with(|| vec![]).push(val) + errors.add(&key, &val); }, ::std::option::Option::None => (), } @@ -159,8 +166,7 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens { let impl_ast = quote!( impl Validate for #ident { fn validate(&self) -> ::std::result::Result<(), ::validator::Errors> { - use std::collections::HashMap; - let mut errors = HashMap::new(); + let mut errors = ::validator::Errors::new(); #(#validations)* @@ -416,7 +422,7 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, S }, _ => panic!("Unexpected word validator: {}", name) }, - // custom + // custom, contains, must_match syn::MetaItem::NameValue(ref name, ref val) => { match name.to_string().as_ref() { "custom" => { @@ -425,6 +431,12 @@ fn find_validators_for_field(field: &syn::Field, field_types: &HashMap<String, 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)), + None => error("invalid argument for `contains` validator: only strings are allowed"), + }; + }, "must_match" => { match lit_to_string(val) { Some(s) => { @@ -555,6 +567,7 @@ fn lit_to_float(lit: &syn::Lit) -> Option<f64> { 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, } diff --git a/validator_derive/tests/test_derive.rs b/validator_derive/tests/test_derive.rs index 9b9bad7..c7e78cc 100644 --- a/validator_derive/tests/test_derive.rs +++ b/validator_derive/tests/test_derive.rs @@ -56,7 +56,7 @@ struct SignupData2 { #[derive(Debug, Validate, Deserialize)] #[validate(schema(function = "validate_signup3"))] struct SignupData3 { - #[validate(email)] + #[validate(email, contains = "bob")] mail: String, #[validate(range(min = "18", max = "20"))] age: u32, @@ -96,7 +96,7 @@ fn test_bad_email_fails_validation() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_err(); + let errs = res.unwrap_err().inner(); assert!(errs.contains_key("mail")); assert_eq!(errs["mail"], vec!["email".to_string()]); } @@ -111,7 +111,7 @@ fn test_bad_url_fails_validation() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_err(); + let errs = res.unwrap_err().inner(); assert!(errs.contains_key("site")); assert_eq!(errs["site"], vec!["url".to_string()]); } @@ -126,8 +126,7 @@ fn test_bad_length_fails_validation_and_points_to_original_name() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_err(); - println!("{:?}", errs); + let errs = res.unwrap_err().inner(); assert!(errs.contains_key("firstName")); assert_eq!(errs["firstName"], vec!["length".to_string()]); } @@ -143,7 +142,7 @@ fn test_bad_range_fails_validation() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_err(); + let errs = res.unwrap_err().inner(); assert!(errs.contains_key("age")); assert_eq!(errs["age"], vec!["range".to_string()]); } @@ -158,7 +157,7 @@ fn test_can_have_multiple_errors() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_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()]); @@ -175,7 +174,7 @@ fn test_custom_validation_error() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_err(); + let errs = res.unwrap_err().inner(); assert!(errs.contains_key("firstName")); assert_eq!(errs["firstName"], vec!["terrible_username".to_string()]); } @@ -209,7 +208,7 @@ fn test_can_fail_struct_validation_new_key() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_err(); + let errs = res.unwrap_err().inner(); assert!(errs.contains_key("all")); assert_eq!(errs["all"], vec!["stupid_rule".to_string()]); } @@ -222,7 +221,7 @@ fn test_can_fail_struct_validation_existing_key() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_err(); + let errs = res.unwrap_err().inner(); assert!(errs.contains_key("mail")); assert_eq!(errs["mail"], vec!["email".to_string(), "stupid_rule".to_string()]); } @@ -235,8 +234,20 @@ fn test_skip_struct_validation_by_default_if_errors() { }; let res = signup.validate(); assert!(res.is_err()); - let errs = res.unwrap_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()]); } |
