diff --git a/README.md b/README.md index e915726b..3b7cb6af 100644 --- a/README.md +++ b/README.md @@ -247,6 +247,16 @@ Examples: #[validate(does_not_contain(pattern = "gmail"))] ``` +### starts_with +Tests whether the string starts with the given pattern. `starts_with` takes 1 string argument. + +Examples: + +```rust +#[validate(starts_with = "gmail")] +#[validate(starts_with(pattern = "gmail"))] +``` + ### regex Tests whether the string matches the regex given. `regex` takes 1 string argument: the path to a static Regex instance. @@ -391,6 +401,7 @@ For example, the following attributes all work: #[validate(custom(function = "custom_fn", code = "code_str"))] #[validate(contains(pattern = "pattern_str", code = "code_str"))] #[validate(does_not_contain(pattern = "pattern_str", code = "code_str"))] +#[validate(starts_with(pattern = "pattern_str", code = "code_str"))] #[validate(must_match(other = "match_value", code = "code_str"))] // message attribute @@ -401,6 +412,7 @@ For example, the following attributes all work: #[validate(custom(function = "custom_fn", message = "message_str"))] #[validate(contains(pattern = "pattern_str", message = "message_str"))] #[validate(does_not_contain(pattern = "pattern_str", message = "message_str"))] +#[validate(starts_with(pattern = "pattern_str", message = "message_str"))] #[validate(must_match(other = "match_value", message = "message_str"))] // both attributes diff --git a/validator/src/lib.rs b/validator/src/lib.rs index 8a7a0c63..4a2c1463 100644 --- a/validator/src/lib.rs +++ b/validator/src/lib.rs @@ -47,6 +47,7 @@ //! | `contains` | | //! | `does_not_contain` | | //! | `custom` | | +//! | `starts_with` | | //! | `regex` | | //! | `credit_card` | (Requires the feature `card` to be enabled) | //! | `phone` | (Requires the feature `phone` to be enabled) | @@ -84,9 +85,10 @@ pub use validation::phone::validate_phone; pub use validation::range::validate_range; pub use validation::required::validate_required; +pub use validation::starts_with::validate_starts_with; pub use validation::urls::validate_url; -pub use traits::{Contains, HasLen, Validate, ValidateArgs}; +pub use traits::{Contains, HasLen, StartsWith, Validate, ValidateArgs}; pub use types::{ValidationError, ValidationErrors, ValidationErrorsKind}; #[cfg(feature = "derive")] diff --git a/validator/src/traits.rs b/validator/src/traits.rs index 05378bc6..ccb9fffc 100644 --- a/validator/src/traits.rs +++ b/validator/src/traits.rs @@ -187,6 +187,37 @@ impl<'a, S, H: ::std::hash::BuildHasher> Contains for &'a HashMap } } +/// Trait to implement if one wants to make the `starts_with` validator +/// work for more types +pub trait StartsWith { + #[must_use] + fn has_element(&self, needle: &str) -> bool; +} + +impl StartsWith for String { + fn has_element(&self, needle: &str) -> bool { + self.starts_with(needle) + } +} + +impl<'a> StartsWith for &'a String { + fn has_element(&self, needle: &str) -> bool { + self.starts_with(needle) + } +} + +impl<'a> StartsWith for &'a str { + fn has_element(&self, needle: &str) -> bool { + self.starts_with(needle) + } +} + +impl<'a> StartsWith for Cow<'a, str> { + fn has_element(&self, needle: &str) -> bool { + self.starts_with(needle) + } +} + /// This is the original trait that was implemented by deriving `Validate`. It will still be /// implemented for struct validations that don't take custom arguments. The call is being /// forwarded to the `ValidateArgs<'v_a>` trait. diff --git a/validator/src/validation/mod.rs b/validator/src/validation/mod.rs index d4307153..eb11844c 100644 --- a/validator/src/validation/mod.rs +++ b/validator/src/validation/mod.rs @@ -12,4 +12,5 @@ pub mod non_control_character; pub mod phone; pub mod range; pub mod required; +pub mod starts_with; pub mod urls; diff --git a/validator/src/validation/starts_with.rs b/validator/src/validation/starts_with.rs new file mode 100644 index 00000000..b0bf0e89 --- /dev/null +++ b/validator/src/validation/starts_with.rs @@ -0,0 +1,44 @@ +use crate::traits::StartsWith; + +/// Validates whether the value starts_with the needle +/// The value needs to implement the StartsWith trait, which is implement on String and str +/// by default. +#[must_use] +pub fn validate_starts_with(val: T, needle: &str) -> bool { + val.has_element(needle) +} + +#[cfg(test)] +mod tests { + use std::borrow::Cow; + + use super::*; + + #[test] + fn test_validate_starts_with_string() { + assert!(validate_starts_with("hey", "h")); + } + + #[test] + fn test_validate_starts_with_string_can_fail() { + assert!(!validate_starts_with("hey", "e")); + assert!(!validate_starts_with("hey", "y")); + assert!(!validate_starts_with("hey", "o")); + } + + #[test] + fn test_validate_starts_with_cow() { + let test: Cow<'static, str> = "hey".into(); + assert!(validate_starts_with(test, "h")); + let test: Cow<'static, str> = String::from("hey").into(); + assert!(validate_starts_with(test, "h")); + } + + #[test] + fn test_validate_starts_with_cow_can_fail() { + let test: Cow<'static, str> = "hey".into(); + assert!(!validate_starts_with(test, "e")); + let test: Cow<'static, str> = String::from("hey").into(); + assert!(!validate_starts_with(test, "e")); + } +} diff --git a/validator_derive/src/lib.rs b/validator_derive/src/lib.rs index fdecfbd1..c4818299 100644 --- a/validator_derive/src/lib.rs +++ b/validator_derive/src/lib.rs @@ -451,7 +451,7 @@ fn find_validators_for_field( } } } - // custom, contains, must_match, regex + // custom, contains, must_match, regex, starts_with syn::Meta::NameValue(syn::MetaNameValue { ref path, ref lit, .. }) => { @@ -493,6 +493,12 @@ fn find_validators_for_field( None => error(lit.span(), "invalid argument for `must_match` validator: only strings are allowed"), }; } + "starts_with" => { + match lit_to_string(lit) { + Some(s) => validators.push(FieldValidation::new(Validator::StartsWith(s))), + None => error(lit.span(), "invalid argument for `starts_with` validator: only strings are allowed"), + }; + } v => abort!( path.span(), "unexpected name value validator: {:?}", @@ -540,7 +546,7 @@ fn find_validators_for_field( &meta_items, )); } - "contains" | "does_not_contain" => { + "contains" | "does_not_contain" | "starts_with" => { validators.push(extract_one_arg_validation( "pattern", ident.to_string(), diff --git a/validator_derive/src/quoting.rs b/validator_derive/src/quoting.rs index 03e40a31..5212aec6 100644 --- a/validator_derive/src/quoting.rs +++ b/validator_derive/src/quoting.rs @@ -488,6 +488,30 @@ pub fn quote_regex_validation( unreachable!(); } +pub fn quote_starts_with_validation( + field_quoter: &FieldQuoter, + validation: &FieldValidation, +) -> proc_macro2::TokenStream { + let field_name = &field_quoter.name; + let validator_param = field_quoter.quote_validator_param(); + + if let Validator::StartsWith(ref needle) = validation.validator { + let quoted_error = quote_error(validation); + let quoted = quote!( + if !::validator::validate_starts_with(#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_nested_validation(field_quoter: &FieldQuoter) -> proc_macro2::TokenStream { let field_name = &field_quoter.name; let validator_field = field_quoter.quote_validator_field(); @@ -520,6 +544,9 @@ pub fn quote_validator( validations.push(quote_contains_validation(field_quoter, validation)) } Validator::Regex(_) => validations.push(quote_regex_validation(field_quoter, validation)), + Validator::StartsWith(_) => { + validations.push(quote_starts_with_validation(field_quoter, validation)) + } #[cfg(feature = "card")] Validator::CreditCard => { validations.push(quote_credit_card_validation(field_quoter, validation)) diff --git a/validator_derive/src/validation.rs b/validator_derive/src/validation.rs index b889ccad..2265736a 100644 --- a/validator_derive/src/validation.rs +++ b/validator_derive/src/validation.rs @@ -373,6 +373,7 @@ pub fn extract_one_arg_validation( "contains" => Validator::Contains(value.unwrap()), "does_not_contain" => Validator::DoesNotContain(value.unwrap()), "must_match" => Validator::MustMatch(value.unwrap()), + "starts_with" => Validator::StartsWith(value.unwrap()), "regex" => Validator::Regex(value.unwrap()), _ => unreachable!(), }; diff --git a/validator_derive_tests/tests/starts_with.rs b/validator_derive_tests/tests/starts_with.rs new file mode 100644 index 00000000..b0b04a78 --- /dev/null +++ b/validator_derive_tests/tests/starts_with.rs @@ -0,0 +1,68 @@ +use validator::Validate; + +#[test] +fn can_validate_starts_with_ok() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(starts_with = "he")] + val: String, + } + + let s = TestStruct { val: "hello".to_string() }; + + assert!(s.validate().is_ok()); +} + +#[test] +fn value_not_starting_with_needle_fails_validation() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(starts_with = "he")] + val: String, + } + + let s = TestStruct { val: String::new() }; + let res = s.validate(); + assert!(res.is_err()); + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "starts_with"); + assert_eq!(errs["val"][0].params["value"], ""); + assert_eq!(errs["val"][0].params["needle"], "he"); +} + +#[test] +fn can_specify_code_for_starts_with() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(starts_with(pattern = "he", code = "oops"))] + val: String, + } + let s = TestStruct { val: String::new() }; + let res = s.validate(); + assert!(res.is_err()); + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].code, "oops"); +} + +#[test] +fn can_specify_message_for_starts_with() { + #[derive(Debug, Validate)] + struct TestStruct { + #[validate(starts_with(pattern = "he", message = "oops"))] + val: String, + } + let s = TestStruct { val: String::new() }; + let res = s.validate(); + assert!(res.is_err()); + let err = res.unwrap_err(); + let errs = err.field_errors(); + assert!(errs.contains_key("val")); + assert_eq!(errs["val"].len(), 1); + assert_eq!(errs["val"][0].clone().message.unwrap(), "oops"); +} diff --git a/validator_types/src/lib.rs b/validator_types/src/lib.rs index 42dbdaff..e1ef7cf2 100644 --- a/validator_types/src/lib.rs +++ b/validator_types/src/lib.rs @@ -21,6 +21,8 @@ pub enum Validator { Contains(String), // No implementation in this crate, it's all in validator_derive Regex(String), + // value is a &str + StartsWith(String), Range { min: Option>, max: Option>, @@ -79,6 +81,7 @@ impl Validator { Validator::Custom { .. } => "custom", Validator::Contains(_) => "contains", Validator::Regex(_) => "regex", + Validator::StartsWith(_) => "starts_with", Validator::Range { .. } => "range", Validator::Length { .. } => "length", #[cfg(feature = "card")]