diff options
| -rw-r--r-- | README.md | 10 | ||||
| -rw-r--r-- | validator/src/lib.rs | 2 | ||||
| -rw-r--r-- | validator/src/must_match.rs | 26 | ||||
| -rw-r--r-- | validator/src/types.rs | 2 | ||||
| -rw-r--r-- | validator_derive/src/lib.rs | 98 | ||||
| -rw-r--r-- | validator_derive/tests/compile-fail/must_match/field_doesnt_exist.rs | 15 | ||||
| -rw-r--r-- | validator_derive/tests/compile-fail/must_match/field_type_doesnt_match.rs | 16 | ||||
| -rw-r--r-- | validator_derive/tests/run-pass/must_match.rs | 18 | ||||
| -rw-r--r-- | validator_derive/tests/test_derive.rs | 27 |
9 files changed, 189 insertions, 25 deletions
@@ -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()) +} |
