From b31fd25cc5cec96ccee737ba1313c6e9f702f32a Mon Sep 17 00:00:00 2001 From: Vincent Prouillet Date: Fri, 26 May 2017 00:49:54 +0900 Subject: Revamp --- validator_derive/src/asserts.rs | 54 +++ validator_derive/src/lib.rs | 672 +++++++++---------------------------- validator_derive/src/lit.rs | 61 ++++ validator_derive/src/quoting.rs | 332 ++++++++++++++++++ validator_derive/src/validation.rs | 290 ++++++++++++++++ 5 files changed, 892 insertions(+), 517 deletions(-) create mode 100644 validator_derive/src/asserts.rs create mode 100644 validator_derive/src/lit.rs create mode 100644 validator_derive/src/quoting.rs create mode 100644 validator_derive/src/validation.rs (limited to 'validator_derive/src') 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", "Option", "Option", "Option", "Option", + "Option", "Option", "Option", "Option", "Option", + "Option", "Option", +]; + + +pub fn assert_string_type(name: &str, field_type: &String) { + if field_type != "String" + && field_type != "&str" + && field_type != "Option" + && !(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")) + && 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", "Option", "Option", "Option", "Option", - "Option", "Option", "Option", "Option", "Option", - "Option", "Option", -]; +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) -> Option { - 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) -> Option) -> HashMap { 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) -> HashMap { } 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, Vec) { - 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, Vec) { + 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| -> 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 { + // 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 match name.to_string().as_ref() { "email" => { - if field_type != "String" - && field_type != "&str" - && field_type != "Option" - && !(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" - && !(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 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")) - && 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) -> Option { let mut original_name = None; @@ -666,9 +364,7 @@ fn find_original_field_name(meta_items: &Vec) -> Option { return find_original_field_name(meta_items); } @@ -684,61 +380,3 @@ fn find_original_field_name(meta_items: &Vec) -> Option Option { - match *lit { - syn::Lit::Str(ref s, _) => Some(s.to_string()), - _ => None, - } -} - -fn lit_to_int(lit: &syn::Lit) -> Option { - match *lit { - syn::Lit::Int(ref s, _) => Some(*s), - // TODO: remove when attr_literals is stable - syn::Lit::Str(ref s, _) => Some(s.parse::().unwrap()), - _ => None, - } -} - -fn lit_to_float(lit: &syn::Lit) -> Option { - match *lit { - syn::Lit::Float(ref s, _) => Some(s.parse::().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::().unwrap()), - _ => None, - } -} - -fn lit_to_bool(lit: &syn::Lit) -> Option { - 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) -> 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 { + match *lit { + syn::Lit::Str(ref s, _) => Some(s.to_string()), + _ => None, + } +} + +pub fn lit_to_int(lit: &syn::Lit) -> Option { + match *lit { + syn::Lit::Int(ref s, _) => Some(*s), + // TODO: remove when attr_literals is stable + syn::Lit::Str(ref s, _) => Some(s.parse::().unwrap()), + _ => None, + } +} + +pub fn lit_to_float(lit: &syn::Lit) -> Option { + match *lit { + syn::Lit::Float(ref s, _) => Some(s.parse::().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::().unwrap()), + _ => None, + } +} + +pub fn lit_to_bool(lit: &syn::Lit) -> Option { + 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) -> 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) -> 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, + pub message: Option, +} + + +#[derive(Debug)] +pub struct FieldValidation { + pub code: String, + pub message: Option, + 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) -> 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) -> 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) -> 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) -> 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, + } +} -- cgit v1.2.3