aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorVincent Prouillet2016-12-29 13:13:42 +0900
committerVincent Prouillet2016-12-29 13:16:41 +0900
commita0f2b4c0d4820bfdcc13c7abdfdc7802d9f6d493 (patch)
tree75b5bc7b008ac374ac0a437de580d5eda1c67a52
parentce9ee1ce4ac73ba7989c88c67b16243cbcec2b98 (diff)
downloadvalidator-a0f2b4c0d4820bfdcc13c7abdfdc7802d9f6d493.tar.bz2
Added must_match
-rw-r--r--README.md10
-rw-r--r--validator/src/lib.rs2
-rw-r--r--validator/src/must_match.rs26
-rw-r--r--validator/src/types.rs2
-rw-r--r--validator_derive/src/lib.rs98
-rw-r--r--validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs15
-rw-r--r--validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs16
-rw-r--r--validator_derive/tests/run-pass/must_match.rs18
-rw-r--r--validator_derive/tests/test_derive.rs27
9 files changed, 189 insertions, 25 deletions
diff --git a/README.md b/README.md
index 0c26d77..62db83c 100644
--- a/README.md
+++ b/README.md
@@ -103,6 +103,16 @@ Examples:
#[validate(range(min = 1.1, max = 10.8))]
```
+### must_match
+Tests whether the 2 fields are equal. `must_match` takes 1 string argument. It will error if the field
+mentioned is missing or has a different type than the field the attribute is on.
+
+Examples:
+
+```rust
+#[validate(must_match = "password2"))]
+```
+
### 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/lib.rs b/validator/src/lib.rs
index 181fc54..3c52661 100644
--- a/validator/src/lib.rs
+++ b/validator/src/lib.rs
@@ -10,6 +10,7 @@ mod email;
mod length;
mod range;
mod urls;
+mod must_match;
pub use types::{Errors, Validate, Validator};
@@ -18,3 +19,4 @@ 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};
diff --git a/validator/src/must_match.rs b/validator/src/must_match.rs
new file mode 100644
index 0000000..35fbee6
--- /dev/null
+++ b/validator/src/must_match.rs
@@ -0,0 +1,26 @@
+/// Validates that the 2 given fields match.
+/// Both fields are optionals
+pub fn validate_must_match<T: Eq>(a: T, b: T) -> bool {
+ a == b
+}
+
+#[cfg(test)]
+mod tests {
+ use super::{validate_must_match};
+
+ #[test]
+ fn test_validate_must_match_strings_valid() {
+ assert!(validate_must_match("hey".to_string(), "hey".to_string()))
+ }
+
+ #[test]
+ fn test_validate_must_match_numbers() {
+ assert!(validate_must_match(2, 2))
+ }
+
+ #[test]
+ fn test_validate_must_match_numbers_false() {
+ assert_eq!(false, validate_must_match(2, 3));
+ }
+
+}
diff --git a/validator/src/types.rs b/validator/src/types.rs
index 8265c63..67e8245 100644
--- a/validator/src/types.rs
+++ b/validator/src/types.rs
@@ -12,6 +12,8 @@ pub trait Validate {
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
diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs
index 0e580b0..cdc244d 100644
--- a/validator_derive/src/lib.rs
+++ b/validator_derive/src/lib.rs
@@ -1,18 +1,19 @@
#![feature(proc_macro, proc_macro_lib)]
#![recursion_limit = "128"]
+
#[macro_use] extern crate quote;
extern crate proc_macro;
extern crate syn;
extern crate validator;
+use std::collections::HashMap;
use proc_macro::TokenStream;
use quote::ToTokens;
use validator::{Validator};
-static LENGTH_TYPES: [&'static str; 2] = ["String", "Vec"];
static RANGE_TYPES: [&'static str; 12] = [
"usize", "u8", "u16", "u32", "u64", "isize", "i8", "i16", "i32", "i64", "f32", "f64"
];
@@ -42,13 +43,15 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens {
let mut validations = vec![];
+ 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);
+ let (name, validators) = find_validators_for_field(field, &field_types);
for validator in &validators {
validations.push(match validator {
&Validator::Length {min, max, equal} => {
@@ -93,6 +96,14 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens {
}
)
},
+ &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.entry(#name.to_string()).or_insert_with(|| vec![]).push("no_match".to_string());
+ }
+ )
+ },
&Validator::Custom(ref f) => {
let fn_ident = syn::Ident::new(f.clone());
quote!(
@@ -127,8 +138,34 @@ fn expand_validation(ast: &syn::MacroInput) -> quote::Tokens {
impl_ast
}
+// Find all 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_type = match field.ty {
+ syn::Ty::Path(_, ref p) => {
+ let mut tokens = quote::Tokens::new();
+ p.to_tokens(&mut tokens);
+ tokens.to_string().replace(' ', "")
+
+ },
+ _ => panic!("Type `{:?}` of field `{}` not supported", field.ty, field_name)
+ };
+ types.insert(field_name, field_type);
+ }
+
+ types
+}
+
/// Find everything we need to know about a Field.
-fn find_validators_for_field(field: &syn::Field) -> (String, Vec<Validator>) {
+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!(),
@@ -137,16 +174,10 @@ fn find_validators_for_field(field: &syn::Field) -> (String, Vec<Validator>) {
let error = |msg: &str| -> ! {
panic!("Invalid attribute #[validate] on field `{}`: {}", field.ident.clone().unwrap().to_string(), msg);
};
-
- let field_type = match field.ty {
- syn::Ty::Path(_, ref p) => {
- p.segments[0].ident.to_string()
-
- },
- _ => error(&format!("Type `{:?}` not supported", field.ty))
- };
+ let field_type = field_types.get(&field_name).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() {
@@ -237,6 +268,10 @@ fn find_validators_for_field(field: &syn::Field) -> (String, Vec<Validator>) {
continue;
}
+ if attr.name() == "validate" {
+ has_validate = true;
+ }
+
match attr.value {
syn::MetaItem::List(_, ref meta_items) => {
if attr.name() == "serde" {
@@ -269,20 +304,37 @@ fn find_validators_for_field(field: &syn::Field) -> (String, Vec<Validator>) {
},
// custom
syn::MetaItem::NameValue(ref name, ref val) => {
- if name == "custom" {
- match lit_to_string(val) {
- Some(s) => validators.push(Validator::Custom(s)),
- None => error("invalid argument for `custom` validator: only strings are allowed"),
- };
- } else {
- panic!("unexpected name value validator: {:?}", name);
- }
+ match name.to_string().as_ref() {
+ "custom" => {
+ match lit_to_string(val) {
+ Some(s) => validators.push(Validator::Custom(s)),
+ None => error("invalid argument for `custom` 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");
+ }
+ },
+ None => error("invalid argument for `must_match` validator: only strings are allowed"),
+ };
+ },
+ _ => 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 !LENGTH_TYPES.contains(&field_type.as_ref()) {
+ if field_type != "String" && !field_type.starts_with("Vec<") {
error(&format!(
"Validator `length` can only be used on types `String` or `Vec` but found `{}`",
field_type
@@ -318,7 +370,7 @@ fn find_validators_for_field(field: &syn::Field) -> (String, Vec<Validator>) {
}
}
- if validators.is_empty() {
+ if has_validate && validators.is_empty() {
error("it needs at least one validator");
}
@@ -359,10 +411,6 @@ fn find_original_field_name(meta_items: &Vec<syn::NestedMetaItem>) -> Option<Str
original_name
}
-//
-//fn quote_length_validator(min: Option<u64>, max: Option<u64>, equal: Option<u64>) {
-//
-//}
fn lit_to_string(lit: &syn::Lit) -> Option<String> {
match *lit {
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
new file mode 100644
index 0000000..03828e2
--- /dev/null
+++ b/validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs
@@ -0,0 +1,15 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `password`: invalid argument for `must_match` validator: field doesn't exist in struct
+struct Test {
+ #[validate(must_match = "password2")]
+ password: String,
+}
+
+fn main() {}
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
new file mode 100644
index 0000000..61c0847
--- /dev/null
+++ b/validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs
@@ -0,0 +1,16 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+//~^ ERROR: custom derive attribute panicked
+//~^^ HELP: Invalid attribute #[validate] on field `password`: invalid argument for `must_match` validator: types of field can't match
+struct Test {
+ #[validate(must_match = "password2")]
+ password: String,
+ password2: i32,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/run-pass/must_match.rs b/validator_derive/tests/run-pass/must_match.rs
new file mode 100644
index 0000000..0d2d917
--- /dev/null
+++ b/validator_derive/tests/run-pass/must_match.rs
@@ -0,0 +1,18 @@
+#![feature(proc_macro, attr_literals)]
+
+#[macro_use] extern crate validator_derive;
+extern crate validator;
+use validator::Validate;
+
+#[derive(Validate)]
+struct Test {
+ #[validate(must_match = "s2")]
+ s: String,
+ s2: String,
+
+ #[validate(must_match = "s4")]
+ s3: usize,
+ s4: usize,
+}
+
+fn main() {}
diff --git a/validator_derive/tests/test_derive.rs b/validator_derive/tests/test_derive.rs
index 1e310b1..7cc63eb 100644
--- a/validator_derive/tests/test_derive.rs
+++ b/validator_derive/tests/test_derive.rs
@@ -21,6 +21,14 @@ struct SignupData {
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());
@@ -134,3 +142,22 @@ fn test_custom_validation_error() {
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())
+}