From e672bb24c7740a08f34b2612d9e7d4dc2a96f6b3 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Thu, 29 Jan 2026 20:06:42 +0100 Subject: [PATCH] Support len_utf16_min and len_utf16_max validations --- CHANGELOG.md | 1 + Cargo.lock | 8 +- README.md | 18 +- examples/no_std_example/Cargo.lock | 8 +- nutype/src/lib.rs | 18 +- nutype_macros/Cargo.toml | 2 +- nutype_macros/src/string/generate/error.rs | 24 +++ nutype_macros/src/string/generate/mod.rs | 30 +++- .../src/string/generate/traits/arbitrary.rs | 4 + nutype_macros/src/string/models.rs | 4 + nutype_macros/src/string/parse.rs | 16 ++ nutype_macros/src/string/validate.rs | 26 +++ test_suite/tests/string.rs | 162 ++++++++++++++++++ .../common/attribute_with_wrong_case.stderr | 2 +- .../len_utf16_min_vs_len_utf16_max.rs | 6 + .../len_utf16_min_vs_len_utf16_max.stderr | 6 + .../tests/ui/string/validate/unknown.stderr | 2 +- 17 files changed, 309 insertions(+), 28 deletions(-) create mode 100644 test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.rs create mode 100644 test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.stderr diff --git a/CHANGELOG.md b/CHANGELOG.md index e0eac02d..381de946 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/Cargo.lock b/Cargo.lock index d7c3e02e..fe12c4dd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -234,18 +234,18 @@ checksum = "453ad9f582a441959e5f0d088b02ce04cfe8d51a8eaf077f12ac6d3e94164ca6" [[package]] name = "kinded" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" +checksum = "ab2109e26a190c55bb0cfc82adaed4ec349a662fa0c76d92ecf321c6e810e2a1" dependencies = [ "kinded_macros", ] [[package]] name = "kinded_macros" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" +checksum = "84b6e58f8fa61844060bdde73407275711897a1fec1d0894b0ee9665e8038143" dependencies = [ "convert_case", "proc-macro2", diff --git a/README.md b/README.md index c3e2d5df..30b4e3ef 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/examples/no_std_example/Cargo.lock b/examples/no_std_example/Cargo.lock index e329b7c4..84596fc8 100644 --- a/examples/no_std_example/Cargo.lock +++ b/examples/no_std_example/Cargo.lock @@ -19,18 +19,18 @@ dependencies = [ [[package]] name = "kinded" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce4bdbb2f423660b19f0e9f7115182214732d8dd5f840cd0a3aee3e22562f34c" +checksum = "ab2109e26a190c55bb0cfc82adaed4ec349a662fa0c76d92ecf321c6e810e2a1" dependencies = [ "kinded_macros", ] [[package]] name = "kinded_macros" -version = "0.3.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a13b4ddc5dcb32f45dac3d6f606da2a52fdb9964a18427e63cd5ef6c0d13288d" +checksum = "84b6e58f8fa61844060bdde73407275711897a1fec1d0894b0ee9665e8038143" dependencies = [ "convert_case", "proc-macro2", diff --git a/nutype/src/lib.rs b/nutype/src/lib.rs index 977fa147..1b4cf296 100644 --- a/nutype/src/lib.rs +++ b/nutype/src/lib.rs @@ -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 //! diff --git a/nutype_macros/Cargo.toml b/nutype_macros/Cargo.toml index 3bac5587..8b82bf49 100644 --- a/nutype_macros/Cargo.toml +++ b/nutype_macros/Cargo.toml @@ -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] diff --git a/nutype_macros/src/string/generate/error.rs b/nutype_macros/src/string/generate/error.rs index fac21b48..cb953203 100644 --- a/nutype_macros/src/string/generate/error.rs +++ b/nutype_macros/src/string/generate/error.rs @@ -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,) } @@ -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)) }, diff --git a/nutype_macros/src/string/generate/mod.rs b/nutype_macros/src/string/generate/mod.rs index 98c879c3..ae421506 100644 --- a/nutype_macros/src/string/generate/mod.rs +++ b/nutype_macros/src/string/generate/mod.rs @@ -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() @@ -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() { @@ -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(()) } diff --git a/nutype_macros/src/string/generate/traits/arbitrary.rs b/nutype_macros/src/string/generate/traits/arbitrary.rs index c17f3961..267d7b75 100644 --- a/nutype_macros/src/string/generate/traits/arbitrary.rs +++ b/nutype_macros/src/string/generate/traits/arbitrary.rs @@ -177,6 +177,10 @@ fn filter_validators(validators: &[StringValidator]) -> Result { + 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() } diff --git a/nutype_macros/src/string/models.rs b/nutype_macros/src/string/models.rs index c1c90d9f..5520f589 100644 --- a/nutype_macros/src/string/models.rs +++ b/nutype_macros/src/string/models.rs @@ -29,6 +29,10 @@ pub type SpannedStringValidator = SpannedItem; pub enum StringValidator { LenCharMin(ValueOrExpr), LenCharMax(ValueOrExpr), + #[kinded(rename = "len_utf16_min")] + LenUtf16Min(ValueOrExpr), + #[kinded(rename = "len_utf16_max")] + LenUtf16Max(ValueOrExpr), NotEmpty, Predicate(TypedCustomFunction), #[cfg_attr(not(feature = "regex"), allow(dead_code))] diff --git a/nutype_macros/src/string/parse.rs b/nutype_macros/src/string/parse.rs index 196888f0..83c61aee 100644 --- a/nutype_macros/src/string/parse.rs +++ b/nutype_macros/src/string/parse.rs @@ -108,6 +108,22 @@ impl Parse for SpannedStringValidator { span, }) } + StringValidatorKind::LenUtf16Min => { + let _: Token![=] = input.parse()?; + let (min_len, span) = parse_number_or_expr::(input)?; + Ok(SpannedStringValidator { + item: StringValidator::LenUtf16Min(min_len), + span, + }) + } + StringValidatorKind::LenUtf16Max => { + let _: Token![=] = input.parse()?; + let (max_len, span) = parse_number_or_expr::(input)?; + Ok(SpannedStringValidator { + item: StringValidator::LenUtf16Max(max_len), + span, + }) + } StringValidatorKind::NotEmpty => Ok(SpannedStringValidator { item: StringValidator::NotEmpty, span: ident.span(), diff --git a/nutype_macros/src/string/validate.rs b/nutype_macros/src/string/validate.rs index 8ced28d2..737f7ecb 100644 --- a/nutype_macros/src/string/validate.rs +++ b/nutype_macros/src/string/validate.rs @@ -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")] diff --git a/test_suite/tests/string.rs b/test_suite/tests/string.rs index d3fcb290..ccf52ceb 100644 --- a/test_suite/tests/string.rs +++ b/test_suite/tests/string.rs @@ -229,6 +229,36 @@ mod validators { assert_eq!(EmailError::NotEmptyViolated.to_string(), "Email is empty."); } + #[test] + fn test_error_display_utf16() { + #[nutype(validate(len_utf16_min = 2, len_utf16_max = 5))] + pub struct JsString(String); + + assert_eq!( + JsStringError::LenUtf16MinViolated.to_string(), + "JsString is too short: the minimum valid UTF-16 length is 2 code units." + ); + assert_eq!( + JsStringError::LenUtf16MaxViolated.to_string(), + "JsString is too long: the maximum valid UTF-16 length is 5 code units." + ); + } + + #[test] + fn test_error_display_utf16_singular() { + #[nutype(validate(len_utf16_min = 1, len_utf16_max = 1))] + pub struct SingleUnit(String); + + assert_eq!( + SingleUnitError::LenUtf16MinViolated.to_string(), + "SingleUnit is too short: the minimum valid UTF-16 length is 1 code unit." + ); + assert_eq!( + SingleUnitError::LenUtf16MaxViolated.to_string(), + "SingleUnit is too long: the maximum valid UTF-16 length is 1 code unit." + ); + } + mod when_boundaries_defined_as_constants { use super::*; @@ -259,6 +289,138 @@ mod validators { ); } } + + #[test] + fn test_len_utf16_max() { + // UTF-16 length validation - useful for JavaScript interop + // 🦀 (crab emoji) is 1 char but 2 UTF-16 code units + #[nutype(validate(len_utf16_max = 5), derive(Debug, PartialEq))] + pub struct ShortText(String); + + // ASCII: 5 chars = 5 UTF-16 code units (valid) + assert_eq!(ShortText::try_new("Anton").unwrap().into_inner(), "Anton"); + + // ASCII: 6 chars = 6 UTF-16 code units (invalid) + assert_eq!( + ShortText::try_new("Serhii"), + Err(ShortTextError::LenUtf16MaxViolated) + ); + + // Emoji: 2 crabs = 2 chars but 4 UTF-16 code units (valid) + assert_eq!(ShortText::try_new("🦀🦀").unwrap().into_inner(), "🦀🦀"); + + // Emoji: 3 crabs = 3 chars but 6 UTF-16 code units (invalid) + assert_eq!( + ShortText::try_new("🦀🦀🦀"), + Err(ShortTextError::LenUtf16MaxViolated) + ); + } + + #[test] + fn test_len_utf16_min() { + // UTF-16 length validation - useful for JavaScript interop + #[nutype(validate(len_utf16_min = 4), derive(Debug, PartialEq))] + pub struct LongText(String); + + // ASCII: 3 chars = 3 UTF-16 code units (invalid) + assert_eq!( + LongText::try_new("abc"), + Err(LongTextError::LenUtf16MinViolated) + ); + + // ASCII: 4 chars = 4 UTF-16 code units (valid) + assert_eq!(LongText::try_new("abcd").unwrap().into_inner(), "abcd"); + + // Emoji: 1 crab = 1 char but 2 UTF-16 code units (invalid) + assert_eq!( + LongText::try_new("🦀"), + Err(LongTextError::LenUtf16MinViolated) + ); + + // Emoji: 2 crabs = 2 chars but 4 UTF-16 code units (valid) + assert_eq!(LongText::try_new("🦀🦀").unwrap().into_inner(), "🦀🦀"); + } + + #[test] + fn test_len_utf16_min_and_max() { + // Combined UTF-16 length validation + #[nutype( + validate(len_utf16_min = 2, len_utf16_max = 4), + derive(Debug, PartialEq) + )] + pub struct BoundedText(String); + + // Too short: 1 UTF-16 code unit + assert_eq!( + BoundedText::try_new("a"), + Err(BoundedTextError::LenUtf16MinViolated) + ); + + // Just right: 2 UTF-16 code units + assert_eq!(BoundedText::try_new("ab").unwrap().into_inner(), "ab"); + + // Just right: 4 UTF-16 code units + assert_eq!(BoundedText::try_new("abcd").unwrap().into_inner(), "abcd"); + + // Too long: 5 UTF-16 code units + assert_eq!( + BoundedText::try_new("abcde"), + Err(BoundedTextError::LenUtf16MaxViolated) + ); + + // 1 emoji = 2 UTF-16 code units (valid) + assert_eq!(BoundedText::try_new("🦀").unwrap().into_inner(), "🦀"); + + // 2 emojis = 4 UTF-16 code units (valid) + assert_eq!(BoundedText::try_new("🦀🦀").unwrap().into_inner(), "🦀🦀"); + + // 3 emojis = 6 UTF-16 code units (invalid) + assert_eq!( + BoundedText::try_new("🦀🦀🦀"), + Err(BoundedTextError::LenUtf16MaxViolated) + ); + } + + mod when_utf16_boundaries_defined_as_constants { + use super::*; + + const MIN_UTF16_LEN: usize = 2; + const MAX_UTF16_LEN: usize = 6; + + #[nutype(validate(len_utf16_min = MIN_UTF16_LEN, len_utf16_max = MAX_UTF16_LEN), derive(Debug))] + struct JsText(String); + + #[test] + fn test_utf16_boundaries_defined_as_constants() { + assert_eq!( + JsText::try_new("a").unwrap_err(), + JsTextError::LenUtf16MinViolated, + ); + assert_eq!( + JsText::try_new("ab").unwrap().into_inner(), + "ab".to_string(), + ); + + assert_eq!( + JsText::try_new("abcdefg").unwrap_err(), + JsTextError::LenUtf16MaxViolated, + ); + assert_eq!( + JsText::try_new("abcdef").unwrap().into_inner(), + "abcdef".to_string(), + ); + + // Test with emoji (2 UTF-16 code units each) + assert_eq!( + JsText::try_new("🦀🦀🦀🦀").unwrap_err(), // 8 UTF-16 code units + JsTextError::LenUtf16MaxViolated, + ); + assert_eq!( + JsText::try_new("🦀🦀🦀").unwrap().into_inner(), // 6 UTF-16 code units + "🦀🦀🦀".to_string(), + ); + } + } } #[cfg(test)] diff --git a/test_suite/tests/ui/common/attribute_with_wrong_case.stderr b/test_suite/tests/ui/common/attribute_with_wrong_case.stderr index 25fb4a1b..e572a3ba 100644 --- a/test_suite/tests/ui/common/attribute_with_wrong_case.stderr +++ b/test_suite/tests/ui/common/attribute_with_wrong_case.stderr @@ -1,5 +1,5 @@ error: Unknown validation attribute: `lenCharMax`. - Possible attributes are `len_char_min`, `len_char_max`, `not_empty`, `predicate`, `regex`, `with`, `error`. + Possible attributes are `len_char_min`, `len_char_max`, `len_utf16_min`, `len_utf16_max`, `not_empty`, `predicate`, `regex`, `with`, `error`. --> tests/ui/common/attribute_with_wrong_case.rs:3:19 | 3 | #[nutype(validate(lenCharMax = 255))] diff --git a/test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.rs b/test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.rs new file mode 100644 index 00000000..31b7e69c --- /dev/null +++ b/test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.rs @@ -0,0 +1,6 @@ +use nutype::nutype; + +#[nutype(validate(len_utf16_min = 127, len_utf16_max = 63))] +pub struct JsText(String); + +fn main () {} diff --git a/test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.stderr b/test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.stderr new file mode 100644 index 00000000..a5595f5f --- /dev/null +++ b/test_suite/tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.stderr @@ -0,0 +1,6 @@ +error: `len_utf16_min` cannot be greater than `len_utf16_max`. + Don't you find this obvious? + --> tests/ui/string/validate/len_utf16_min_vs_len_utf16_max.rs:3:56 + | +3 | #[nutype(validate(len_utf16_min = 127, len_utf16_max = 63))] + | ^^ diff --git a/test_suite/tests/ui/string/validate/unknown.stderr b/test_suite/tests/ui/string/validate/unknown.stderr index 10941f6f..4fdee22f 100644 --- a/test_suite/tests/ui/string/validate/unknown.stderr +++ b/test_suite/tests/ui/string/validate/unknown.stderr @@ -1,5 +1,5 @@ error: Unknown validation attribute: `unique`. - Possible attributes are `len_char_min`, `len_char_max`, `not_empty`, `predicate`, `regex`, `with`, `error`. + Possible attributes are `len_char_min`, `len_char_max`, `len_utf16_min`, `len_utf16_max`, `not_empty`, `predicate`, `regex`, `with`, `error`. --> tests/ui/string/validate/unknown.rs:3:19 | 3 | #[nutype(validate(unique))]