Skip to content
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
Expand Up @@ -2,6 +2,7 @@
- **[BREAKING]** Rename `derive_unsafe` to `derive_unchecked` (both the feature flag and the attribute).
- **[FEATURE]** Ability to derive [`Valuable`](https://docs.rs/valuable/0.1.1/valuable/trait.Valuable.html) (requires `valuable` feature).
- **[FEATURE]** Ability to control constructor visibility with `constructor(visibility = ...)` attribute (see [#211](https://github.com/greyblake/nutype/issues/211)).
- **[FEATURE]** Add `len_utf16_min` and `len_utf16_max` validators for string types to validate UTF-16 code unit length (useful for JavaScript interop) (see [#162](https://github.com/greyblake/nutype/issues/162)).

### v0.6.2 - 2025-06-30
- **[FEATURE]** Introduce `derive_unsafe(..)` attribute to derive any arbitrary trait (requires `derive_unsafe` feature to be enabled).
Expand Down
8 changes: 4 additions & 4 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -91,14 +91,16 @@ At the moment the string inner type supports only `String` (owned) type.

### String validators

| Validator | Description | Error variant | Example |
|----------------|---------------------------------------------------------------------------------|----------------------|----------------------------------------------|
| `len_char_min` | Min length of the string (in chars, not bytes) | `LenCharMinViolated` | `len_char_min = 5` |
| `len_char_max` | Max length of the string (in chars, not bytes) | `LenCharMaxViolated` | `len_char_max = 255` |
| `not_empty` | Rejects an empty string | `NotEmptyViolated` | `not_empty` |
| `regex` | Validates format with a regex. Requires `regex` feature. | `RegexViolated` | `regex = "^[0-9]{7}$"` or `regex = ID_REGEX` |
| `predicate` | Custom validator. A function or closure that receives `&str` and returns `bool` | `PredicateViolated` | `predicate = \|s: &str\| s.contains('@')` |
| `with` | Custom validator with a custom error | N/A | (see example below) |
| Validator | Description | Error variant | Example |
|-----------------|---------------------------------------------------------------------------------|-----------------------|----------------------------------------------|
| `len_char_min` | Min length of the string (in chars, not bytes) | `LenCharMinViolated` | `len_char_min = 5` |
| `len_char_max` | Max length of the string (in chars, not bytes) | `LenCharMaxViolated` | `len_char_max = 255` |
| `len_utf16_min` | Min length of the string in UTF-16 code units (useful for JavaScript interop) | `LenUtf16MinViolated` | `len_utf16_min = 5` |
| `len_utf16_max` | Max length of the string in UTF-16 code units (useful for JavaScript interop) | `LenUtf16MaxViolated` | `len_utf16_max = 255` |
| `not_empty` | Rejects an empty string | `NotEmptyViolated` | `not_empty` |
| `regex` | Validates format with a regex. Requires `regex` feature. | `RegexViolated` | `regex = "^[0-9]{7}$"` or `regex = ID_REGEX` |
| `predicate` | Custom validator. A function or closure that receives `&str` and returns `bool` | `PredicateViolated` | `predicate = \|s: &str\| s.contains('@')` |
| `with` | Custom validator with a custom error | N/A | (see example below) |


#### Regex validation
Expand Down
8 changes: 4 additions & 4 deletions examples/no_std_example/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

18 changes: 10 additions & 8 deletions nutype/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,14 +110,16 @@
//!
//! ### String validators
//!
//! | Validator | Description | Error variant | Example |
//! |----------------|---------------------------------------------------------------------------------|----------------------|----------------------------------------------|
//! | `len_char_min` | Min length of the string (in chars, not bytes) | `LenCharMinViolated` | `len_char_min = 5` |
//! | `len_char_max` | Max length of the string (in chars, not bytes) | `LenCharMaxViolated` | `len_char_max = 255` |
//! | `not_empty` | Rejects an empty string | `NotEmptyViolated` | `not_empty` |
//! | `regex` | Validates format with a regex. Requires `regex` feature. | `RegexViolated` | `regex = "^[0-9]{7}$"` or `regex = ID_REGEX` |
//! | `predicate` | Custom validator. A function or closure that receives `&str` and returns `bool` | `PredicateViolated` | `predicate = \|s: &str\| s.contains('@')` |
//! | `with` | Custom validator with a custom error | N/A | (see example below) |
//! | Validator | Description | Error variant | Example |
//! |-----------------|---------------------------------------------------------------------------------|-----------------------|----------------------------------------------|
//! | `len_char_min` | Min length of the string (in chars, not bytes) | `LenCharMinViolated` | `len_char_min = 5` |
//! | `len_char_max` | Max length of the string (in chars, not bytes) | `LenCharMaxViolated` | `len_char_max = 255` |
//! | `len_utf16_min` | Min length of the string in UTF-16 code units (useful for JavaScript interop) | `LenUtf16MinViolated` | `len_utf16_min = 5` |
//! | `len_utf16_max` | Max length of the string in UTF-16 code units (useful for JavaScript interop) | `LenUtf16MaxViolated` | `len_utf16_max = 255` |
//! | `not_empty` | Rejects an empty string | `NotEmptyViolated` | `not_empty` |
//! | `regex` | Validates format with a regex. Requires `regex` feature. | `RegexViolated` | `regex = "^[0-9]{7}$"` or `regex = ID_REGEX` |
//! | `predicate` | Custom validator. A function or closure that receives `&str` and returns `bool` | `PredicateViolated` | `predicate = \|s: &str\| s.contains('@')` |
//! | `with` | Custom validator with a custom error | N/A | (see example below) |
//!
//! #### Regex validation
//!
Expand Down
2 changes: 1 addition & 1 deletion nutype_macros/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ syn = { version = "2.0", features = ["extra-traits", "full"] }
# as `regex = "^foo|bar$"` can be compiled to a Regex without errors.
regex = { version = "1", optional = true }
cfg-if = "1.0"
kinded = "0.3.0"
kinded = "0.4.1"
urlencoding = "2.0"

[build-dependencies]
Expand Down
24 changes: 24 additions & 0 deletions nutype_macros/src/string/generate/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,12 @@ fn gen_definition(error_type_path: &ErrorTypePath, validators: &[StringValidator
StringValidator::LenCharMin(_len) => {
quote!(LenCharMinViolated,)
}
StringValidator::LenUtf16Max(_len) => {
quote!(LenUtf16MaxViolated,)
}
StringValidator::LenUtf16Min(_len) => {
quote!(LenUtf16MinViolated,)
}
StringValidator::NotEmpty => {
quote!(NotEmptyViolated,)
}
Expand Down Expand Up @@ -81,6 +87,24 @@ fn gen_impl_display_trait(
if #len_char_min == 1 { "" } else { "s" }
)
},
StringValidator::LenUtf16Max(len_utf16_max) => quote! {
#error_type_path::LenUtf16MaxViolated => write!(
f,
"{} is too long: the maximum valid UTF-16 length is {} code unit{}.",
stringify!(#type_name),
#len_utf16_max,
if #len_utf16_max == 1 { "" } else { "s" }
)
},
StringValidator::LenUtf16Min(len_utf16_min) => quote! {
#error_type_path::LenUtf16MinViolated => write!(
f,
"{} is too short: the minimum valid UTF-16 length is {} code unit{}.",
stringify!(#type_name),
#len_utf16_min,
if #len_utf16_min == 1 { "" } else { "s" }
)
},
StringValidator::NotEmpty => quote! {
#error_type_path::NotEmptyViolated => write!(f, "{} is empty.", stringify!(#type_name))
},
Expand Down
30 changes: 29 additions & 1 deletion nutype_macros/src/string/generate/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,11 @@ impl GenerateNewtype for StringNewtype {
const_fn: ConstFn,
) -> TokenStream {
// Indicates that `chars_count` variable needs to be set, which is used within
// min_len and max_len validations.
// len_char_min and len_char_max validations.
let mut requires_chars_count = false;
// Indicates that `utf16_count` variable needs to be set, which is used within
// len_utf16_min and len_utf16_max validations.
let mut requires_utf16_count = false;

let validations: TokenStream = validators
.iter()
Expand All @@ -109,6 +112,22 @@ impl GenerateNewtype for StringNewtype {
}
)
}
StringValidator::LenUtf16Max(max_len) => {
requires_utf16_count = true;
quote!(
if utf16_count > #max_len {
return Err(#error_type_path::LenUtf16MaxViolated);
}
)
}
StringValidator::LenUtf16Min(min_len) => {
requires_utf16_count = true;
quote!(
if utf16_count < #min_len {
return Err(#error_type_path::LenUtf16MinViolated);
}
)
}
StringValidator::NotEmpty => {
quote!(
if val.is_empty() {
Expand Down Expand Up @@ -156,9 +175,18 @@ impl GenerateNewtype for StringNewtype {
quote!()
};

let utf16_count_if_required = if requires_utf16_count {
quote!(
let utf16_count = val.encode_utf16().count();
)
} else {
quote!()
};

quote!(
#const_fn fn __validate__(val: &str) -> ::core::result::Result<(), #error_type_path> {
#chars_count_if_required
#utf16_count_if_required
#validations
Ok(())
}
Expand Down
4 changes: 4 additions & 0 deletions nutype_macros/src/string/generate/traits/arbitrary.rs
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ fn filter_validators(validators: &[StringValidator]) -> Result<Vec<RelevantValid
let msg = "It's not possible to derive `Arbitrary` trait for a type with `regex` validator.\nYou have to implement `Arbitrary` trait on you own.";
Err(syn::Error::new(Span::call_site(), msg))
}
StringValidator::LenUtf16Min(_) | StringValidator::LenUtf16Max(_) => {
let msg = "It's not possible to derive `Arbitrary` trait for a type with `len_utf16_min` or `len_utf16_max` validator.\nYou have to implement `Arbitrary` trait on you own.";
Err(syn::Error::new(Span::call_site(), msg))
}
}
}).collect()
}
Expand Down
4 changes: 4 additions & 0 deletions nutype_macros/src/string/models.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,10 @@ pub type SpannedStringValidator = SpannedItem<StringValidator>;
pub enum StringValidator {
LenCharMin(ValueOrExpr<usize>),
LenCharMax(ValueOrExpr<usize>),
#[kinded(rename = "len_utf16_min")]
LenUtf16Min(ValueOrExpr<usize>),
#[kinded(rename = "len_utf16_max")]
LenUtf16Max(ValueOrExpr<usize>),
NotEmpty,
Predicate(TypedCustomFunction),
#[cfg_attr(not(feature = "regex"), allow(dead_code))]
Expand Down
16 changes: 16 additions & 0 deletions nutype_macros/src/string/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,22 @@ impl Parse for SpannedStringValidator {
span,
})
}
StringValidatorKind::LenUtf16Min => {
let _: Token![=] = input.parse()?;
let (min_len, span) = parse_number_or_expr::<usize>(input)?;
Ok(SpannedStringValidator {
item: StringValidator::LenUtf16Min(min_len),
span,
})
}
StringValidatorKind::LenUtf16Max => {
let _: Token![=] = input.parse()?;
let (max_len, span) = parse_number_or_expr::<usize>(input)?;
Ok(SpannedStringValidator {
item: StringValidator::LenUtf16Max(max_len),
span,
})
}
StringValidatorKind::NotEmpty => Ok(SpannedStringValidator {
item: StringValidator::NotEmpty,
span: ident.span(),
Expand Down
26 changes: 26 additions & 0 deletions nutype_macros/src/string/validate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,32 @@ fn validate_validators(
return Err(err);
}

// len_utf16_max VS len_utf16_min
//
let maybe_len_utf16_min = validators
.iter()
.flat_map(|v| match v.item {
StringValidator::LenUtf16Min(ValueOrExpr::Value(len)) => Some((v.span, len)),
_ => None,
})
.next();
let maybe_len_utf16_max = validators
.iter()
.flat_map(|v| match v.item {
StringValidator::LenUtf16Max(ValueOrExpr::Value(len)) => Some((v.span, len)),
_ => None,
})
.next();
if let (Some((_, len_utf16_min)), Some((len_utf16_max_span, len_utf16_max))) =
(maybe_len_utf16_min, maybe_len_utf16_max)
&& len_utf16_min > len_utf16_max
{
let msg =
"`len_utf16_min` cannot be greater than `len_utf16_max`.\nDon't you find this obvious?";
let err = syn::Error::new(len_utf16_max_span, msg);
return Err(err);
}

// Validate regex
//
#[cfg(feature = "regex")]
Expand Down
Loading
Loading