Skip to content

Commit

Permalink
Merge pull request #38 from greyblake/len-in-chars
Browse files Browse the repository at this point in the history
Make max_len and mix_len check length in chars, not bytes
  • Loading branch information
greyblake authored Jun 23, 2023
2 parents aa812cf + f309dde commit 9f25afb
Show file tree
Hide file tree
Showing 6 changed files with 31 additions and 70 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
### v0.3.0 - 2023-??-??
* [BREAKING] `min_len` and `max_len` validators run against number of characters in a string (`val.chars().count()`), not number of bytes (`val.len()`).
* Add `finite` validation for float types which checks against NaN and infinity.
* Enable deriving of `Eq` and `Ord` on float types (if `finite` validation is present)
* Enable deriving of `TryFrom` for types without validation (in this case Error type is `std::convert::Infallible`)
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,8 +127,8 @@ At the moment the string inner type supports only `String` (owned) type.

| Validator | Description | Error variant | Example |
|-------------|---------------------------------------------------------------------------------|-----------------|----------------------------------------------|
| `max_len` | Max length of the string | `TooLong` | `max_len = 255` |
| `min_len` | Min length of the string | `TooShort` | `min_len = 5` |
| `max_len` | Max length of the string (in chars, not bytes) | `TooLong` | `max_len = 255` |
| `min_len` | Min length of the string (in chars, not bytes) | `TooShort` | `min_len = 5` |
| `not_empty` | Rejects an empty string | `Empty` | `not_empty` |
| `regex` | Validates format with a regex. Requires `regex` feature. | `RegexMismatch` | `regex = "^[0-9]{7}$"` or `regex = ID_REGEX` |
| `with` | Custom validator. A function or closure that receives `&str` and returns `bool` | `Invalid` | `with = \|s: &str\| s.contains('@')` |
Expand Down
67 changes: 3 additions & 64 deletions dummy/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,67 +1,6 @@
use nutype::nutype;
use schemars::JsonSchema;
#[nutype(validate(finite, max = 12.34))]
#[derive(FromStr, Display, Clone, Copy, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
pub struct Dist(f64);

#[nutype(
new_unchecked
validate(min = 18, max = 99)
)]
#[derive(FromStr, Display, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct Age(u8);
#[nutype(validate(min_len = 3, max_len = 255))]
pub struct Name(String);

#[nutype(new_unchecked)]
#[derive(Debug, FromStr, Display, Clone, Serialize, JsonSchema)]
pub struct Username(String);

use lazy_static::lazy_static;
use regex::Regex;

lazy_static! {
static ref PIN_CODE_REGEX_LAZY_STATIC: Regex = Regex::new("^[0-9]{4}$").unwrap();
}

#[nutype(validate(regex = PIN_CODE_REGEX_LAZY_STATIC))]
// #[nutype(validate(regex = "^[0-9]{4}$"))]
#[derive(Debug)]
pub struct PinCode(String);

#[nutype(
new_unchecked
validate(finite)
)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
pub struct Coefficient(f64);

fn main() {
let dist: Dist = "11.4".parse().unwrap();
println!("dist = {}", dist.into_inner());

let age: Age = "77".parse().unwrap();
let json = serde_json::to_string(&age).unwrap();
println!("AGE JSON = {json}");

let username: Username = "greyblake".parse().unwrap();
let json = serde_json::to_string(&username).unwrap();
println!("USERNAME JSON = {json}");

let dist: Dist = serde_json::from_str("12.339999999999").unwrap();
println!("Dist = {dist}");

let name = "Bang".to_string();

let username = unsafe { Username::new_unchecked(name) };
println!("{username:#?}");

let pin_result = PinCode::new("1223 ");
println!("\npin_result = {pin_result:?}\n");

let k1 = Coefficient::new(0.0).unwrap();
let k2 = Coefficient::new(1.21).unwrap();
let k3 = Coefficient::new(3.21).unwrap();

let mut ks = [k3, k1, k2, k1, k3];
ks.sort();
println!("{ks:?}");
}
fn main() {}
4 changes: 2 additions & 2 deletions nutype/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -121,8 +121,8 @@
//!
//! | Validator | Description | Error variant | Example |
//! |-------------|---------------------------------------------------------------------------------|-----------------|----------------------------------------------|
//! | `max_len` | Max length of the string | `TooLong` | `max_len = 255` |
//! | `min_len` | Min length of the string | `TooShort` | `min_len = 5` |
//! | `max_len` | Max length of the string (in chars, not bytes) | `TooLong` | `max_len = 255` |
//! | `min_len` | Min length of the string (in chars, not bytes) | `TooShort` | `min_len = 5` |
//! | `not_empty` | Rejects an empty string | `Empty` | `not_empty` |
//! | `regex` | Validates format with a regex. Requires `regex` feature. | `RegexMismatch` | `regex = "^[0-9]{7}$"` or `regex = ID_REGEX` |
//! | `with` | Custom validator. A function or closure that receives `&str` and returns `bool` | `Invalid` | `with = \|s: &str\| s.contains('@')` |
Expand Down
19 changes: 17 additions & 2 deletions nutype_macros/src/string/gen/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,19 +177,25 @@ pub fn gen_string_sanitize_fn(sanitizers: &[StringSanitizer]) -> TokenStream {
pub fn gen_string_validate_fn(type_name: &TypeName, validators: &[StringValidator]) -> TokenStream {
let error_name = gen_error_type_name(type_name);

// Indicates that `chars_count` variable needs to be set, which is used within
// min_len and max_len validations.
let mut requires_chars_count = false;

let validations: TokenStream = validators
.iter()
.map(|validator| match validator {
StringValidator::MaxLen(max_len) => {
requires_chars_count = true;
quote!(
if val.len() > #max_len {
if chars_count > #max_len {
return Err(#error_name::TooLong);
}
)
}
StringValidator::MinLen(min_len) => {
requires_chars_count = true;
quote!(
if val.len() < #min_len {
if chars_count < #min_len {
return Err(#error_name::TooShort);
}
)
Expand Down Expand Up @@ -237,8 +243,17 @@ pub fn gen_string_validate_fn(type_name: &TypeName, validators: &[StringValidato
})
.collect();

let chars_count_if_required = if requires_chars_count {
quote!(
let chars_count = val.chars().count();
)
} else {
quote!()
};

quote!(
fn validate(val: &str) -> ::core::result::Result<(), #error_name> {
#chars_count_if_required
#validations
Ok(())
}
Expand Down
6 changes: 6 additions & 0 deletions test_suite/tests/string.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,9 @@ mod validators {

assert_eq!(Name::new("Anton").unwrap().into_inner(), "Anton");
assert_eq!(Name::new("Serhii"), Err(NameError::TooLong));

// Ukranian, Cyrillic. Every char is 2 bytes.
assert_eq!(Name::new("Антон").unwrap().into_inner(), "Антон");
}

#[test]
Expand All @@ -109,6 +112,9 @@ mod validators {

assert_eq!(Name::new("Anton"), Err(NameError::TooShort));
assert_eq!(Name::new("Serhii").unwrap().into_inner(), "Serhii");

// Ukranian, Cyrillic. Every char is 2 bytes.
assert_eq!(Name::new("Антон"), Err(NameError::TooShort));
}

#[test]
Expand Down

0 comments on commit 9f25afb

Please sign in to comment.