aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--README.md10
-rw-r--r--validator/src/contains.rs70
-rw-r--r--validator/src/length.rs6
-rw-r--r--validator/src/lib.rs2
-rw-r--r--validator/src/types.rs29
-rw-r--r--validator_derive/Cargo.toml4
-rw-r--r--validator_derive/src/lib.rs35
-rw-r--r--validator_derive/tests/test_derive.rs33
8 files changed, 161 insertions, 28 deletions
diff --git a/README.md b/README.md
index e99a54f..37946ee 100644
--- a/README.md
+++ b/README.md
@@ -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()]);
}