Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add starts_with validator #260

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion validator/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) |
Expand Down Expand Up @@ -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")]
Expand Down
31 changes: 31 additions & 0 deletions validator/src/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,37 @@ impl<'a, S, H: ::std::hash::BuildHasher> Contains for &'a HashMap<String, S, H>
}
}

/// 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.
Expand Down
1 change: 1 addition & 0 deletions validator/src/validation/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
44 changes: 44 additions & 0 deletions validator/src/validation/starts_with.rs
Original file line number Diff line number Diff line change
@@ -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<T: StartsWith>(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"));
}
}
10 changes: 8 additions & 2 deletions validator_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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, ..
}) => {
Expand Down Expand Up @@ -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: {:?}",
Expand Down Expand Up @@ -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(),
Expand Down
27 changes: 27 additions & 0 deletions validator_derive/src/quoting.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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))
Expand Down
1 change: 1 addition & 0 deletions validator_derive/src/validation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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!(),
};
Expand Down
68 changes: 68 additions & 0 deletions validator_derive_tests/tests/starts_with.rs
Original file line number Diff line number Diff line change
@@ -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");
}
3 changes: 3 additions & 0 deletions validator_types/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<ValueOrPath<f64>>,
max: Option<ValueOrPath<f64>>,
Expand Down Expand Up @@ -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")]
Expand Down