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

Make max_len and mix_len check length in chars, not bytes #38

Merged
merged 1 commit into from
Jun 23, 2023
Merged
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
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