diff --git a/.cargo/config.toml b/.cargo/config.toml index 9f0a63e..9f04996 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,4 +1,5 @@ [alias] +build-xtask = "build --manifest-path ./xtask/Cargo.toml" xtask = "run --quiet --manifest-path ./xtask/Cargo.toml --" x = "run --quiet --manifest-path ./xtask/Cargo.toml --" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4217698..aa8afa9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,7 +41,7 @@ jobs: shared-key: "rust-stable-test" - name: Install wasm-bindgen-cli - run: cargo install --locked wasm-bindgen-cli + run: cargo install wasm-bindgen-cli - name: Build xtask run: cargo build --manifest-path ./xtask/Cargo.toml @@ -88,7 +88,7 @@ jobs: shared-key: "rust-${{ matrix.build }}-test" - name: Build xtask - run: cargo build --manifest-path ./xtask/Cargo.toml + run: cargo build-xtask - name: Build docs run: cargo doc --all-features @@ -124,7 +124,7 @@ jobs: shared-key: "rust-checks" - name: Build xtask - run: cargo build --manifest-path ./xtask/Cargo.toml + run: cargo build-xtask - name: Run checks run: cargo x check diff --git a/README.md b/README.md index 1960d97..80603b8 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ A Rust validation library - [Basic usage example](#basic-usage-example) - [Validation rules](#available-validation-rules) +- [Length modes](#length-modes) - [Inner type validation](#inner-type-validation) - [Newtypes](#newtypes) - [Handling Option](#handling-option) @@ -74,35 +75,34 @@ if let Err(e) = data.validate(&()) { ### Available validation rules -| name | format | validation | feature flag | -| ------------ | ------------------------------------------------ | ---------------------------------------------------- | -------------- | -| required | `#[garde(required)]` | is value set | - | -| ascii | `#[garde(ascii)]` | only contains ASCII | - | -| alphanumeric | `#[garde(alphanumeric)]` | only letters and digits | - | -| email | `#[garde(email)]` | an email according to the HTML5 spec[^1] | `email` | -| url | `#[garde(url)]` | a URL | `url` | -| ip | `#[garde(ip)]` | an IP address (either IPv4 or IPv6) | - | -| ipv4 | `#[garde(ipv4)]` | an IPv4 address | - | -| ipv6 | `#[garde(ipv6)]` | an IPv6 address | - | -| credit card | `#[garde(credit_card)]` | a credit card number | `credit-card` | -| phone number | `#[garde(phone_number)]` | a phone number | `phone-number` | -| length | `#[garde(length(min=, max=)]` | a container with length in `min..=max` | - | -| byte_length | `#[garde(byte_length(min=, max=)]` | a byte sequence with length in `min..=max` | - | -| range | `#[garde(range(min=, max=))]` | a number in the range `min..=max` | - | -| contains | `#[garde(contains())]` | a string-like value containing a substring | - | -| prefix | `#[garde(prefix())]` | a string-like value prefixed by some string | - | -| suffix | `#[garde(suffix())]` | a string-like value suffixed by some string | - | -| pattern | `#[garde(pattern(""))]` | a string-like value matching some regular expression | `regex` | -| pattern | `#[garde(pattern())]` | a string-like value matched by some [Matcher](https://docs.rs/garde/latest/garde/rules/pattern/trait.Matcher.html) | - | -| dive | `#[garde(dive)]` | nested validation, calls `validate` on the value | - | -| skip | `#[garde(skip)]` | skip validation | - | -| custom | `#[garde(custom())]` | a custom validator | - | +| name | format | validation | feature flag | +| ------------ | --------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ | -------------- | +| required | `#[garde(required)]` | is value set | - | +| ascii | `#[garde(ascii)]` | only contains ASCII | - | +| alphanumeric | `#[garde(alphanumeric)]` | only letters and digits | - | +| email | `#[garde(email)]` | an email according to the HTML5 spec[^1] | `email` | +| url | `#[garde(url)]` | a URL | `url` | +| ip | `#[garde(ip)]` | an IP address (either IPv4 or IPv6) | - | +| ipv4 | `#[garde(ipv4)]` | an IPv4 address | - | +| ipv6 | `#[garde(ipv6)]` | an IPv6 address | - | +| credit card | `#[garde(credit_card)]` | a credit card number | `credit-card` | +| phone number | `#[garde(phone_number)]` | a phone number | `phone-number` | +| length | `#[garde(length(, min=, max=)]` | a container with length in `min..=max` | - | +| range | `#[garde(range(min=, max=))]` | a number in the range `min..=max` | - | +| contains | `#[garde(contains())]` | a string-like value containing a substring | - | +| prefix | `#[garde(prefix())]` | a string-like value prefixed by some string | - | +| suffix | `#[garde(suffix())]` | a string-like value suffixed by some string | - | +| pattern | `#[garde(pattern(""))]` | a string-like value matching some regular expression | `regex` | +| pattern | `#[garde(pattern())]` | a string-like value matched by some [Matcher](https://docs.rs/garde/latest/garde/rules/pattern/trait.Matcher.html) | - | +| dive | `#[garde(dive)]` | nested validation, calls `validate` on the value | - | +| skip | `#[garde(skip)]` | skip validation | - | +| custom | `#[garde(custom())]` | a custom validator | - | Additional notes: - `required` is only available for `Option` fields. - For `length` and `range`, either `min` or `max` may be omitted, but not both. - `length` and `range` use an *inclusive* upper bound (`min..=max`). -- `length` uses `.chars().count()` for UTF-8 strings instead of `.len()`. +- The `` argument for `length` is [explained here](#length-modes) - For `contains`, `prefix`, and `suffix`, the pattern must be a string literal, because the `Pattern` API [is currently unstable](https://github.com/rust-lang/rust/issues/27721). - Garde does not enable the default features of the `regex` crate - if you need extra regex features (e.g. Unicode) or better performance, add a dependency on `regex = "1"` to your `Cargo.toml`. @@ -129,6 +129,55 @@ struct Bar<'a> { } ``` +### Length modes + +The `length` rule accepts an optional `mode` argument, which determines what _kind_ of length it will validate. + +The options are: +- `simple` +- `bytes` +- `graphemes` +- `utf16` +- `chars` + +The `simple` is the default used when the `mode` argument is omitted. The meaning of "simple length" +depends on the type. It is currently implemented for strings, where it validates the number of bytes, +and `std::collections`, where it validates the number of items. + +```rust +#[derive(garde::Validate)] +struct Foo { + #[garde(length(min = 1, max = 100))] + string: String, + + #[garde(length(min = 1, max = 100))] + collection: Vec +} +``` + +The `bytes`, `graphemes`, `utf16`, and `chars` exist mostly for string validation: +- `bytes` validates the number of _bytes_ +- `graphemens` uses the [`unicode-segmentation`](https://docs.rs/unicode-segmentation) crate, and validates the number of _graphemes_ +- `utf16` uses [`encode_utf16`](https://doc.rust-lang.org/stable/std/primitive.str.html#method.encode_utf16), and validates the number of UTF-16 _code points_ +- `chars` uses [`chars`](https://doc.rust-lang.org/stable/std/primitive.str.html#method.chars), and validates the number of _unicode scalar values_ + +```rust +#[derive(garde::Validate)] +struct Foo { + #[garde(length(bytes, min = 1, max = 100))] + a: String, // `a.len()` + + #[garde(length(graphemes, min = 1, max = 100))] + b: String, // `b.graphemes().count()` + + #[garde(length(utf16, min = 1, max = 100))] + c: String, // `c.encode_utf16().count()` + + #[garde(length(chars, min = 1, max = 100))] + d: String, // `d.chars().count()` +} +``` + ### Inner type validation If you need to validate the "inner" type of a container, such as the `String` in `Vec`, then use the `inner` modifier: @@ -267,15 +316,16 @@ struct User { ### Implementing rules Say you want to implement length checking for a custom string-like type. -To do this, you would implement the `garde::rules::length::HasLength` trait for it. +To do this, you would implement one of the `length` traits for it, depending +on what kind of validation you are looking for. ```rust #[repr(transparent)] -pub struct MyString(pub String); +pub struct MyString(String); -impl garde::rules::length::HasLength for MyString { +impl garde::rules::length::HasSimpleLength for MyString { fn length(&self) -> usize { - self.0.chars().count() + self.0.len() } } #[derive(garde::Validate)] @@ -335,16 +385,16 @@ struct Bar { ### Feature flags -| name | description | extra dependencies | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| -| `derive` | Enables the usage of the `derive(Validate)` macro | [`garde_derive`](https://crates.io/crates/garde_derive) | -| `url` | Validation of URLs via the `url` crate. | [`url`](https://crates.io/crates/url) | -| `email` | Validation of emails according to [HTML5](https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address) | [`regex`](https://crates.io/crates/regex), [`once_cell`](https://crates.io/crates/once_cell) | -| `email-idna` | Support for [Internationalizing Domain Names for Applications](https://url.spec.whatwg.org/#idna) in email addresses | [`idna`](https://crates.io/crates/idna) | -| `regex` | Support for regular expressions in `pattern` via the `regex` crate | [`regex`](https://crates.io/crates/regex), [`once_cell`](https://crates.io/crates/once_cell) | -| `credit-card` | Validation of credit card numbers via the `card-validate` crate | [`card-validate`](https://crates.io/crates/card-validate) | -| `phone-number` | Validation of phone numbers via the `phonenumber` crate | [`phonenumber`](https://crates.io/crates/phonenumber) | - +| name | description | extra dependencies | +| -------------- | -------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------- | +| `derive` | Enables the usage of the `derive(Validate)` macro | [`garde_derive`](https://crates.io/crates/garde_derive) | +| `url` | Validation of URLs via the `url` crate. | [`url`](https://crates.io/crates/url) | +| `email` | Validation of emails according to [HTML5](https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address) | [`regex`](https://crates.io/crates/regex), [`once_cell`](https://crates.io/crates/once_cell) | +| `email-idna` | Support for [Internationalizing Domain Names for Applications](https://url.spec.whatwg.org/#idna) in email addresses | [`idna`](https://crates.io/crates/idna) | +| `regex` | Support for regular expressions in `pattern` via the `regex` crate | [`regex`](https://crates.io/crates/regex), [`once_cell`](https://crates.io/crates/once_cell) | +| `credit-card` | Validation of credit card numbers via the `card-validate` crate | [`card-validate`](https://crates.io/crates/card-validate) | +| `phone-number` | Validation of phone numbers via the `phonenumber` crate | [`phonenumber`](https://crates.io/crates/phonenumber) | +| `unicode` | Validation of grapheme count via the `unicode-segmentation` crate | [`unicode-segmentation`](https://docs.rs/unicode-segmentation) | ### Why `garde`? @@ -364,9 +414,9 @@ This repository also makes use of the following tools, but they are optional: Licensed under either of - Apache License, Version 2.0 - ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) + ([LICENSE-APACHE](LICENSE-APACHE) or ) - MIT license - ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) + ([LICENSE-MIT](LICENSE-MIT) or ) at your option. diff --git a/garde/Cargo.toml b/garde/Cargo.toml index 36dcda2..8ddabec 100644 --- a/garde/Cargo.toml +++ b/garde/Cargo.toml @@ -20,10 +20,12 @@ default = [ "email", "email-idna", "regex", + "unicode", ] serde = ["dep:serde", "compact_str/serde"] derive = ["dep:garde_derive"] url = ["dep:url"] +unicode = ["dep:unicode-segmentation"] credit-card = ["dep:card-validate"] phone-number = ["dep:phonenumber"] email = ["regex"] @@ -40,6 +42,7 @@ compact_str = { version = "0.7.1", default-features = false } serde = { version = "1", features = ["derive"], optional = true } url = { version = "2", optional = true } +unicode-segmentation = { version = "1.10.1", optional = true } card-validate = { version = "2.3", optional = true } phonenumber = { version = "0.3.2+8.13.9", optional = true } regex = { version = "1", default-features = false, features = [ @@ -59,14 +62,3 @@ glob = "0.3.1" [target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dev-dependencies] wasm-bindgen-test = "0.3.38" - -[target.'cfg(not(all(target_arch = "wasm32", target_os = "unknown")))'.dev-dependencies] -criterion = "0.4" - -[[bench]] -name = "validation" -harness = false - -[profile.profiling] -inherits = "release" -debug = true diff --git a/garde/README.md b/garde/README.md deleted file mode 100644 index 3810e30..0000000 --- a/garde/README.md +++ /dev/null @@ -1,381 +0,0 @@ -# Garde   [![Documentation]][docs.rs] [![Latest Version]][crates.io] - -[docs.rs]: https://docs.rs/garde/latest/garde/ -[crates.io]: https://crates.io/crates/garde -[Documentation]: https://img.shields.io/docsrs/garde -[Latest Version]: https://img.shields.io/crates/v/garde.svg - -A Rust validation library - -- [Basic usage example](#basic-usage-example) -- [Validation rules](#available-validation-rules) -- [Inner type validation](#inner-type-validation) -- [Handling Option](#handling-option) -- [Custom validation](#custom-validation) -- [Custom validation with containers](#custom-validation-with-containers) -- [Context/Self access](#contextself-access) -- [Implementing rules](#implementing-rules) -- [Implementing `Validate`](#implementing-validate) -- [Integration with web frameworks](#integration-with-web-frameworks) -- [Feature flags](#feature-flags) -- [Why `garde`?](#why-garde) - -### Basic usage example - -To get started, use the `Validate` derive macro and add some validation rules to your type. -This generates an implementation of the `Validate` trait for you. -To use it, call the `validate` method on an instance of the type. - -Here's what that looks like in full: - -```rust -use garde::{Validate, Valid}; - -#[derive(Validate)] -struct User<'a> { - #[garde(ascii, length(min=3, max=25))] - username: &'a str, - #[garde(length(min=15))] - password: &'a str, -} - -let user = User { - username: "test", - password: "not_a_very_good_password", -}; - -if let Err(e) = user.validate(&()) { - println!("invalid user: {e}"); -} -``` - -Garde can also validate enums: - -```rust -use garde::{Validate, Valid}; - -#[derive(Validate)] -enum Data { - Struct { - #[garde(range(min=-10, max=10))] - field: i32, - }, - Tuple( - #[garde(ascii)] - String - ), -} - -let data = Data::Struct { field: 100 }; -if let Err(e) = data.validate(&()) { - println!("invalid data: {e}"); -} -``` - -### Available validation rules - -| name | format | validation | feature flag | -| ------------ | ------------------------------------------------ | ---------------------------------------------------- | -------------- | -| required | `#[garde(required)]` | is value set | - | -| ascii | `#[garde(ascii)]` | only contains ASCII | - | -| alphanumeric | `#[garde(alphanumeric)]` | only letters and digits | - | -| email | `#[garde(email)]` | an email according to the HTML5 spec[^1] | `email` | -| url | `#[garde(url)]` | a URL | `url` | -| ip | `#[garde(ip)]` | an IP address (either IPv4 or IPv6) | - | -| ipv4 | `#[garde(ipv4)]` | an IPv4 address | - | -| ipv6 | `#[garde(ipv6)]` | an IPv6 address | - | -| credit card | `#[garde(credit_card)]` | a credit card number | `credit-card` | -| phone number | `#[garde(phone_number)]` | a phone number | `phone-number` | -| length | `#[garde(length(min=, max=)]` | a container with length in `min..=max` | - | -| byte_length | `#[garde(byte_length(min=, max=)]` | a byte sequence with length in `min..=max` | - | -| range | `#[garde(range(min=, max=))]` | a number in the range `min..=max` | - | -| contains | `#[garde(contains())]` | a string-like value containing a substring | - | -| prefix | `#[garde(prefix())]` | a string-like value prefixed by some string | - | -| suffix | `#[garde(suffix())]` | a string-like value suffixed by some string | - | -| pattern | `#[garde(pattern(""))]` | a string-like value matching some regular expression | `regex` | -| pattern | `#[garde(pattern())]` | a string-like value matched by some [Matcher](https://docs.rs/garde/latest/garde/rules/pattern/trait.Matcher.html) | - | -| dive | `#[garde(dive)]` | nested validation, calls `validate` on the value | - | -| skip | `#[garde(skip)]` | skip validation | - | -| custom | `#[garde(custom())]` | a custom validator | - | - -Additional notes: -- `required` is only available for `Option` fields. -- For `length` and `range`, either `min` or `max` may be omitted, but not both. -- `length` and `range` use an *inclusive* upper bound (`min..=max`). -- `length` uses `.chars().count()` for UTF-8 strings instead of `.len()`. -- For `contains`, `prefix`, and `suffix`, the pattern must be a string literal, because the `Pattern` API [is currently unstable](https://github.com/rust-lang/rust/issues/27721). -- Garde does not enable the default features of the `regex` crate - if you need extra regex features (e.g. Unicode) or better performance, add a dependency on `regex = "1"` to your `Cargo.toml`. - -If most of the fields on your struct are annotated with `#[garde(skip)]`, you may use `#[garde(allow_unvalidated)]` instead: - -```rust -#[derive(garde::Validate)] -struct Foo<'a> { - #[garde(length(min = 1))] - a: &'a str, - - #[garde(skip)] - b: &'a str, // this field will not be validated -} - -#[derive(garde::Validate)] -#[garde(allow_unvalidated)] -struct Bar<'a> { - #[garde(length(min = 1))] - a: &'a str, - - b: &'a str, // this field will not be validated - // note the lack of `#[garde(skip)]` -} -``` - -### Inner type validation - -If you need to validate the "inner" type of a container, such as the `String` in `Vec`, then use the `inner` modifier: - -```rust -#[derive(garde::Validate)] -struct Test { - #[garde( - length(min = 1), - inner(ascii, length(min = 1)), // wrap the rule in `inner` - )] - items: Vec, -} -``` - -The above type would fail validation if: -- the `Vec` is empty -- any of the inner `String` elements is empty -- any of the inner `String` elements contains non-ASCII characters - -### Handling Option - -Every rule works on `Option` fields. The field will only be validated if it is `Some`. If you additionally want to validate that the `Option` field is `Some`, use the `required` rule: - -```rust -#[derive(garde::Validate)] -struct Test { - #[garde(required, ascii, length(min = 1))] - value: Option, -} -``` - -The above type would fail validation if: -- `value` is `None` -- the inner `value` is empty -- the inner `value` contains non-ASCII characters - -### Custom validation - -Validation may be customized via the `custom` rule, and the `context` attribute. - -The context may be any type without generic parameters. By default, the context is `()`. - -```rust,ignore -#[derive(garde::Validate)] -#[garde(context(PasswordContext))] -struct User { - #[garde(custom(is_strong_password))] - password: String, -} - -struct PasswordContext { - min_entropy: f32, - entropy: cracken::password_entropy::EntropyEstimator, -} - -fn is_strong_password(value: &str, context: &PasswordContext) -> garde::Result { - let bits = context.entropy.estimate_password_entropy(value.as_bytes()) - .map(|e| e.mask_entropy) - .unwrap_or(0.0); - if bits < context.min_entropy { - return Err(garde::Error::new("password is not strong enough")); - } - Ok(()) -} - -let ctx = PasswordContext { /* ... */ }; -let user = User { /* ... */ }; -user.validate(&ctx)?; -``` - -The validator function may accept the value as a reference to any type which it derefs to. -In the above example, it is possible to use `&str`, because `password` is a `String`, and `String` derefs to `&str`. - -### Custom validation with containers - -When working with custom validators, if the type is a container such as `Vec` or `Option`, the validation function will get a reference to that container instead of the underlying data. This is in contrast with built-in validators that are able to extract the type from some container types such as `Option`. -To validate the underlying data of a container when using a custom validator, use the `inner` modifier: - -```rust,ignore -#[derive(garde::Validate)] -#[garde(context(PasswordContext))] -struct User { - #[garde(inner(custom(is_strong_password)))] // wrap the rule in `inner` - password: Option, // this field will only be validated if it is the `Some` variant -} - -struct PasswordContext { - min_entropy: f32, - entropy: cracken::password_entropy::EntropyEstimator, -} - -fn is_strong_password(value: &str, context: &PasswordContext) -> garde::Result { - let bits = context.entropy.estimate_password_entropy(value.as_bytes()) - .map(|e| e.mask_entropy) - .unwrap_or(0.0); - if bits < context.min_entropy { - return Err(garde::Error::new("password is not strong enough")); - } - Ok(()) -} - -let ctx = PasswordContext { /* ... */ }; -let user = User { /* ... */ }; -user.validate(&ctx)?; -``` - -The above type will always pass validation if the `password` field is `None`. -This allows you to use the same validation function for `T` as you do for `Option` or `Vec`. - -### Context/Self access - -It's generally possible to also access the context and `self`, because they are in scope in the output of the proc macro: -```rust -struct Limits { - min: usize, - max: usize, -} - -struct Config { - username: Limits, -} - -#[derive(garde::Validate)] -#[garde(context(Config as ctx))] -struct User { - #[garde(length(min = ctx.username.min, max = ctx.username.max))] - username: String, -} -``` - -### Implementing rules - -Say you want to implement length checking for a custom string-like type. -To do this, you would implement the `garde::rules::length::HasLength` trait for it. - -```rust -#[repr(transparent)] -pub struct MyString(pub String); - -impl garde::rules::length::HasLength for MyString { - fn length(&self) -> usize { - self.0.chars().count() - } -} -#[derive(garde::Validate)] -struct Foo { - // Now the `length` check may be used with `MyString` - #[garde(length(min = 1, max = 1000))] - field: MyString, -} -``` - -Each rule comes with its own trait that may be implemented by custom types in your code. -They are all available under `garde::rules`. - -### Implementing `Validate` - -In case you have a container type for which you'd like to support nested validation (using the `#[garde(dive)]` rule), -you may implement `Validate` for it: - -```rust -#[repr(transparent)] -struct MyVec(Vec); - -impl garde::Validate for MyVec { - type Context = T::Context; - - fn validate_into( - &self, - ctx: &Self::Context, - mut parent: &mut dyn FnMut() -> garde::Path, - report: &mut garde::Report - ) { - for (index, item) in self.0.iter().enumerate() { - let mut path = garde::util::nested_path!(parent, index); - item.validate_into(ctx, &mut path, report); - } - } -} - -#[derive(garde::Validate)] -struct Foo { - #[garde(dive)] - field: MyVec, -} - -#[derive(garde::Validate)] -struct Bar { - #[garde(range(min = 1, max = 10))] - value: u32, -} -``` - -### Integration with web frameworks - -- [`axum`](https://crates.io/crates/axum): [`axum_garde`](https://crates.io/crates/axum_garde) -- [`actix-web`](https://crates.io/crates/actix-web): [`garde-actix-web`](https://crates.io/crates/garde-actix-web) - -### Feature flags - - -| name | description | extra dependencies | -|--------------------------|-----------------------------------------------------------------------------------------------------------------------------------|----------------------------------------------------------------------------------------------| -| `derive` | Enables the usage of the `derive(Validate)` macro | [`garde_derive`](https://crates.io/crates/garde_derive) | -| `url` | Validation of URLs via the `url` crate. | [`url`](https://crates.io/crates/url) | -| `email` | Validation of emails according to [HTML5](https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address) | [`regex`](https://crates.io/crates/regex), [`once_cell`](https://crates.io/crates/once_cell) | -| `email-idna` | Support for [Internationalizing Domain Names for Applications](https://url.spec.whatwg.org/#idna) in email addresses | [`idna`](https://crates.io/crates/idna) | -| `regex` | Support for regular expressions in `pattern` via the `regex` crate | [`regex`](https://crates.io/crates/regex), [`once_cell`](https://crates.io/crates/once_cell) | -| `credit-card` | Validation of credit card numbers via the `card-validate` crate | [`card-validate`](https://crates.io/crates/card-validate) | -| `phone-number` | Validation of phone numbers via the `phonenumber` crate | [`phonenumber`](https://crates.io/crates/phonenumber) | - - -### Why `garde`? - -Garde means guard in French. I am not French, nor do I speak the language, but `guard` was taken, and this is close enough :). - -### Development - -Contributing to `garde` only requires a somewhat recent version of [`Rust`](https://www.rust-lang.org/learn/get-started). - -This repository also makes use of the following tools, but they are optional: -- [`insta`](https://insta.rs/) for snapshot testing ([tests/rules](./garde_derive_tests/tests/rules/)). -- [`just`](https://just.systems/) for running recipes defined in the [`justfile`](./justfile). - Run `just -l` to see what recipes are available. - -### License - -Licensed under either of - -- Apache License, Version 2.0 - ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0) -- MIT license - ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT) - -at your option. - -### Contribution - -Unless you explicitly state otherwise, any contribution intentionally submitted -for inclusion in the work by you, as defined in the Apache-2.0 license, shall be -dual licensed as above, without any additional terms or conditions. - -### Acknowledgements - -This crate is heavily inspired by the [validator](https://github.com/Keats/validator) crate. It is essentially a full rewrite of `validator`. -The creation of this crate was prompted by [this comment](https://github.com/Keats/validator/issues/201#issuecomment-1167018511) -and a few others talking about a potential rewrite. - -[^1]: [HTML5 forms - valid email address](https://html.spec.whatwg.org/multipage/forms.html#valid-e-mail-address) diff --git a/garde/README.md b/garde/README.md new file mode 120000 index 0000000..32d46ee --- /dev/null +++ b/garde/README.md @@ -0,0 +1 @@ +../README.md \ No newline at end of file diff --git a/garde/benches/validation.rs b/garde/benches/validation.rs deleted file mode 100644 index c00ef24..0000000 --- a/garde/benches/validation.rs +++ /dev/null @@ -1,178 +0,0 @@ -use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use garde::Validate; - -#[derive(Debug, garde::Validate)] -struct Test<'a> { - #[garde(alphanumeric)] - alphanumeric: Option<&'a str>, - #[garde(ascii)] - ascii: Option<&'a str>, - #[garde(byte_length(min = 1))] - byte_length_min1_str: Option<&'a str>, - #[garde(byte_length(min = 1))] - byte_length_min1_u8_slice: Option<&'a [u8]>, - #[garde(contains("a"))] - contains_a: Option<&'a str>, - #[garde(credit_card)] - credit_card: Option<&'a str>, - #[garde(email)] - email: Option<&'a str>, - #[garde(ip)] - ip: Option<&'a str>, - #[garde(length(min = 1))] - length_min1: Option<&'a str>, - #[garde(pattern(r"a|b"))] - pat_a_or_b: Option<&'a str>, - #[garde(phone_number)] - phone_number: Option<&'a str>, - #[garde(prefix("a"))] - prefix_a: Option<&'a str>, - #[garde(range(min = 1))] - range_min1: Option, - #[garde(required)] - required: Option<&'a str>, - #[garde(suffix("a"))] - suffix_a: Option<&'a str>, - #[garde(url)] - url: Option<&'a str>, - #[garde(dive)] - nested: Option>>, -} - -macro_rules! valid_input { - () => { - Test { - alphanumeric: Some("a"), - ascii: Some("a"), - byte_length_min1_str: Some("a"), - byte_length_min1_u8_slice: Some(&[0]), - contains_a: Some("a"), - credit_card: Some("4539571147647251"), - email: Some("test@mail.com"), - ip: Some("127.0.0.1"), - length_min1: Some("a"), - pat_a_or_b: Some("a"), - phone_number: Some("+14152370800"), - prefix_a: Some("a"), - range_min1: Some(1), - required: Some("a"), - suffix_a: Some("a"), - url: Some("http://test.com"), - nested: None, - } - }; - ($nested:expr) => { - Test { - alphanumeric: Some("a"), - ascii: Some("a"), - byte_length_min1_str: Some("a"), - byte_length_min1_u8_slice: Some(&[0]), - contains_a: Some("a"), - credit_card: Some("4539571147647251"), - email: Some("test@mail.com"), - ip: Some("127.0.0.1"), - length_min1: Some("a"), - pat_a_or_b: Some("a"), - phone_number: Some("+14152370800"), - prefix_a: Some("a"), - range_min1: Some(1), - required: Some("a"), - suffix_a: Some("a"), - url: Some("http://test.com"), - nested: Some(Box::new($nested)), - } - }; -} - -macro_rules! invalid_input { - () => { - Test { - alphanumeric: Some("๐Ÿ˜‚"), - ascii: Some("๐Ÿ˜‚"), - byte_length_min1_str: Some(""), - byte_length_min1_u8_slice: Some(&[]), - contains_a: Some("๐Ÿ˜‚"), - credit_card: Some("๐Ÿ˜‚"), - email: Some("๐Ÿ˜‚"), - ip: Some("๐Ÿ˜‚"), - length_min1: Some(""), - pat_a_or_b: Some("๐Ÿ˜‚"), - phone_number: Some("๐Ÿ˜‚"), - prefix_a: Some(""), - range_min1: Some(0), - required: None, - suffix_a: Some("๐Ÿ˜‚"), - url: Some("๐Ÿ˜‚"), - nested: None, - } - }; - ($nested:expr) => { - Test { - alphanumeric: Some("๐Ÿ˜‚"), - ascii: Some("๐Ÿ˜‚"), - byte_length_min1_str: Some(""), - byte_length_min1_u8_slice: Some(&[]), - contains_a: Some("๐Ÿ˜‚"), - credit_card: Some("๐Ÿ˜‚"), - email: Some("๐Ÿ˜‚"), - ip: Some("๐Ÿ˜‚"), - length_min1: Some(""), - pat_a_or_b: Some("๐Ÿ˜‚"), - phone_number: Some("๐Ÿ˜‚"), - prefix_a: Some(""), - range_min1: Some(0), - required: None, - suffix_a: Some("๐Ÿ˜‚"), - url: Some("๐Ÿ˜‚"), - nested: Some(Box::new($nested)), - } - }; -} - -fn validate(c: &mut Criterion) { - let inputs = vec![ - ( - "valid", - valid_input!(valid_input!(valid_input!(valid_input!()))), - ), - ( - "invalid", - invalid_input!(invalid_input!(invalid_input!(invalid_input!()))), - ), - ]; - - for (name, input) in inputs { - c.bench_function(&format!("validate `{name}`"), |b| { - b.iter(|| { - let _ = black_box(input.validate(&())); - }) - }); - } -} - -fn display(c: &mut Criterion) { - let inputs = vec![ - ( - "valid", - valid_input!(valid_input!(valid_input!(valid_input!()))).validate(&()), - ), - ( - "invalid", - invalid_input!(invalid_input!(invalid_input!(invalid_input!()))).validate(&()), - ), - ]; - - for (name, input) in inputs { - c.bench_function(&format!("display `{name}`"), |b| { - b.iter(|| { - let _ = black_box(match &input { - Ok(()) => String::new(), - Err(e) => e.to_string(), - }); - }) - }); - } -} - -criterion_group!(benches, validate, display); -criterion_main!(benches); diff --git a/garde/src/error.rs b/garde/src/error.rs index d12ecb3..f3e82ee 100644 --- a/garde/src/error.rs +++ b/garde/src/error.rs @@ -17,7 +17,7 @@ use self::rc_list::List; /// It is a flat list of `(Path, Error)`. /// A single field or list item may have any number of errors attached to it. /// -/// It is possible to extract all errors for specific field using the [`select`] macro. +/// It is possible to extract all errors for specific field using the [`select`][`crate::select`] macro. #[derive(Debug)] #[cfg_attr(feature = "serde", derive(serde::Serialize))] pub struct Report { diff --git a/garde/src/rules/byte_length.rs b/garde/src/rules/byte_length.rs deleted file mode 100644 index 8cf17c4..0000000 --- a/garde/src/rules/byte_length.rs +++ /dev/null @@ -1,116 +0,0 @@ -//! Byte length validation. -//! -//! ```rust -//! #[derive(garde::Validate)] -//! struct Test { -//! #[garde(byte_length(min=1, max=100))] -//! v: String, -//! } -//! ``` -//! -//! The entrypoint is the [`ByteLength`] trait. Implementing this trait for a type allows that type to be used with the `#[garde(byte_length(...))]` rule. -//! -//! The [`ByteLength`] has a companion trait [`HasByteLength`], which may be implemented for any container with a known length counted in bytes. -//! [`ByteLength`] is implemented for any `T: HasByteLength`. -//! -//! In case of string types, [`HasByteLength::byte_length`] should return the number of _bytes_ as opposed to the number of _characters_. -//! For validation of length counted in _characters_, see the [`crate::rules::length`] rule. -//! -//! Here's what implementing the trait for a custom string-like type might look like: -//! ```rust -//! #[repr(transparent)] -//! struct MyString(String); -//! -//! impl garde::rules::byte_length::HasByteLength for MyString { -//! fn byte_length(&self) -> usize { -//! self.0.len() -//! } -//! } -//! ``` - -use super::AsStr; -use crate::error::Error; - -pub fn apply(v: &T, (min, max): (usize, usize)) -> Result<(), Error> { - if let Err(e) = v.validate_byte_length(min, max) { - match e { - InvalidLength::Min => { - return Err(Error::new(format!("byte length is lower than {min}"))) - } - InvalidLength::Max => { - return Err(Error::new(format!("byte length is greater than {max}"))) - } - } - } - Ok(()) -} - -pub trait ByteLength { - fn validate_byte_length(&self, min: usize, max: usize) -> Result<(), InvalidLength>; -} - -pub enum InvalidLength { - Min, - Max, -} - -#[allow(clippy::len_without_is_empty)] -pub trait HasByteLength { - fn byte_length(&self) -> usize; -} - -impl ByteLength for T { - fn validate_byte_length(&self, min: usize, max: usize) -> Result<(), InvalidLength> { - let len = HasByteLength::byte_length(self); - if len < min { - Err(InvalidLength::Min) - } else if len > max { - Err(InvalidLength::Max) - } else { - Ok(()) - } - } -} - -impl ByteLength for Option { - fn validate_byte_length(&self, min: usize, max: usize) -> Result<(), InvalidLength> { - match self { - Some(value) => value.validate_byte_length(min, max), - None => Ok(()), - } - } -} - -impl HasByteLength for T { - fn byte_length(&self) -> usize { - self.as_byte_slice().len() - } -} - -pub trait AsByteSlice { - fn as_byte_slice(&self) -> &[u8]; -} - -impl<'a> AsByteSlice for &'a [u8] { - fn as_byte_slice(&self) -> &[u8] { - self - } -} - -impl AsByteSlice for Vec { - fn as_byte_slice(&self) -> &[u8] { - self.as_slice() - } -} - -impl AsByteSlice for [u8; N] { - fn as_byte_slice(&self) -> &[u8] { - self - } -} - -impl AsByteSlice for T { - fn as_byte_slice(&self) -> &[u8] { - self.as_str().as_bytes() - } -} diff --git a/garde/src/rules/length.rs b/garde/src/rules/length.rs index 447cd95..172a655 100644 --- a/garde/src/rules/length.rs +++ b/garde/src/rules/length.rs @@ -8,191 +8,65 @@ //! } //! ``` //! -//! The entrypoint is the [`Length`] trait. Implementing this trait for a type allows that type to be used with the `#[garde(length(...))]` rule. +//! The concept of "length" is somewhat complicated, especially for strings. Therefore, the `length` rule currently supports different modes: +//! - [`Simple`][simple::Simple], which is the default +//! - [`Bytes`][bytes::Bytes] +//! - [`Chars`][chars::Chars] +//! - [`Graphemes`][graphemes::Graphemes] +//! - [`Utf16CodeUnits`][utf16::Utf16CodeUnits] //! -//! The [`Length`] has a companion trait [`HasLength`], which may be implemented for any container with a known length. -//! [`Length`] is implemented for any `T: HasLength`. -//! -//! In case of string types, [`HasLength::length`] should return the number of _characters_ as opposed to the number of _bytes_. -//! For validation of length counted in _bytes_, see the [`crate::rules::byte_length`] rule. +//! The mode is configured on the `length` rule: +//! ```rust +//! #[derive(garde::Validate)] +//! struct Test { +//! #[garde( +//! length(graphemes, min=1, max=25), +//! length(bytes, min=1, max=100), +//! )] +//! v: String, +//! } +//! ``` //! //! Here's what implementing the trait for a custom string-like type might look like: //! ```rust //! #[repr(transparent)] //! struct MyString(String); //! -//! impl garde::rules::length::HasLength for MyString { +//! impl garde::rules::length::HasSimpleLength for MyString { //! fn length(&self) -> usize { -//! self.0.chars().count() +//! self.0.len() //! } //! } //! ``` //! +//! See each trait for more information. +//! -use crate::error::Error; - -pub fn apply(v: &T, (min, max): (usize, usize)) -> Result<(), Error> { - if let Err(e) = v.validate_length(min, max) { - match e { - InvalidLength::Min => return Err(Error::new(format!("length is lower than {min}"))), - InvalidLength::Max => return Err(Error::new(format!("length is greater than {max}"))), - } - } - Ok(()) -} - -pub trait Length { - fn validate_length(&self, min: usize, max: usize) -> Result<(), InvalidLength>; -} - -pub enum InvalidLength { - Min, - Max, -} - -#[allow(clippy::len_without_is_empty)] -pub trait HasLength { - fn length(&self) -> usize; -} - -impl Length for T { - fn validate_length(&self, min: usize, max: usize) -> Result<(), InvalidLength> { - let len = HasLength::length(self); - if len < min { - Err(InvalidLength::Min) - } else if len > max { - Err(InvalidLength::Max) - } else { - Ok(()) - } - } -} - -impl Length for Option { - fn validate_length(&self, min: usize, max: usize) -> Result<(), InvalidLength> { - match self { - Some(value) => value.validate_length(min, max), - None => Ok(()), - } - } -} - -impl HasLength for String { - fn length(&self) -> usize { - self.chars().count() - } -} - -impl<'a> HasLength for &'a String { - fn length(&self) -> usize { - self.chars().count() - } -} - -impl<'a> HasLength for &'a str { - fn length(&self) -> usize { - self.chars().count() - } -} - -impl<'a> HasLength for std::borrow::Cow<'a, str> { - fn length(&self) -> usize { - self.len() - } -} - -impl HasLength for Vec { - fn length(&self) -> usize { - self.len() - } -} - -impl<'a, T> HasLength for &'a Vec { - fn length(&self) -> usize { - self.len() - } -} - -impl HasLength for &[T] { - fn length(&self) -> usize { - self.len() - } -} - -impl HasLength for [T; N] { - fn length(&self) -> usize { - N - } -} - -impl HasLength for &[T; N] { - fn length(&self) -> usize { - N - } -} - -impl<'a, K, V, S> HasLength for &'a std::collections::HashMap { - fn length(&self) -> usize { - self.len() - } -} - -impl HasLength for std::collections::HashMap { - fn length(&self) -> usize { - self.len() - } -} - -impl<'a, T, S> HasLength for &'a std::collections::HashSet { - fn length(&self) -> usize { - self.len() - } -} - -impl HasLength for std::collections::HashSet { - fn length(&self) -> usize { - self.len() - } -} +pub mod bytes; +pub use bytes::HasBytes; -impl<'a, K, V> HasLength for &'a std::collections::BTreeMap { - fn length(&self) -> usize { - self.len() - } -} +pub mod chars; +pub use chars::HasChars; -impl HasLength for std::collections::BTreeMap { - fn length(&self) -> usize { - self.len() - } -} +#[cfg(feature = "unicode")] +pub mod graphemes; +#[cfg(feature = "unicode")] +pub use graphemes::HasGraphemes; -impl<'a, T> HasLength for &'a std::collections::BTreeSet { - fn length(&self) -> usize { - self.len() - } -} +pub mod simple; +pub use simple::HasSimpleLength; -impl HasLength for std::collections::BTreeSet { - fn length(&self) -> usize { - self.len() - } -} +pub mod utf16; +pub use utf16::HasUtf16CodeUnits; -impl HasLength for std::collections::VecDeque { - fn length(&self) -> usize { - self.len() - } -} - -impl HasLength for std::collections::BinaryHeap { - fn length(&self) -> usize { - self.len() - } -} +use crate::error::Error; -impl HasLength for std::collections::LinkedList { - fn length(&self) -> usize { - self.len() +fn check_len(len: usize, min: usize, max: usize) -> Result<(), Error> { + if len < min { + Err(Error::new(format!("length is lower than {min}"))) + } else if len > max { + Err(Error::new(format!("length is greater than {max}"))) + } else { + Ok(()) } } diff --git a/garde/src/rules/length/bytes.rs b/garde/src/rules/length/bytes.rs new file mode 100644 index 0000000..543db5b --- /dev/null +++ b/garde/src/rules/length/bytes.rs @@ -0,0 +1,61 @@ +//! Implemented by types for which we can retrieve the number of bytes. +//! +//! See also: [`chars` on `str`](https://doc.rust-lang.org/std/primitive.str.html#method.chars). + +use crate::error::Error; + +pub fn apply(v: &T, (min, max): (usize, usize)) -> Result<(), Error> { + v.validate_num_bytes(min, max) +} + +pub trait Bytes { + fn validate_num_bytes(&self, min: usize, max: usize) -> Result<(), Error>; +} + +impl Bytes for T { + fn validate_num_bytes(&self, min: usize, max: usize) -> Result<(), Error> { + super::check_len(self.num_bytes(), min, max) + } +} + +impl Bytes for Option { + fn validate_num_bytes(&self, min: usize, max: usize) -> Result<(), Error> { + match self { + Some(v) => v.validate_num_bytes(min, max), + None => Ok(()), + } + } +} + +pub trait HasBytes { + fn num_bytes(&self) -> usize; +} + +macro_rules! impl_via_len { + ($(in<$lifetime:lifetime>)? $T:ty) => { + impl<$($lifetime)?> HasBytes for $T { + fn num_bytes(&self) -> usize { + self.len() + } + } + }; +} + +impl_via_len!(std::string::String); +impl_via_len!(in<'a> &'a std::string::String); +impl_via_len!(in<'a> &'a str); +impl_via_len!(in<'a> std::borrow::Cow<'a, str>); +impl_via_len!(std::rc::Rc); +impl_via_len!(std::sync::Arc); +impl_via_len!(std::boxed::Box); +impl_via_len!(in<'a> &'a [u8]); +impl_via_len!(std::rc::Rc<[u8]>); +impl_via_len!(std::sync::Arc<[u8]>); +impl_via_len!(std::boxed::Box<[u8]>); +impl_via_len!(std::vec::Vec); + +impl HasBytes for [u8; N] { + fn num_bytes(&self) -> usize { + self.len() + } +} diff --git a/garde/src/rules/length/chars.rs b/garde/src/rules/length/chars.rs new file mode 100644 index 0000000..b479366 --- /dev/null +++ b/garde/src/rules/length/chars.rs @@ -0,0 +1,66 @@ +//! Implemented by string-like types for which we can retrieve the number of [Unicode Scalar Values](https://www.unicode.org/glossary/#unicode_scalar_value). +//! +//! See also: [`chars` on `str`](https://doc.rust-lang.org/std/primitive.str.html#method.chars). + +use crate::error::Error; + +pub fn apply(v: &T, (min, max): (usize, usize)) -> Result<(), Error> { + v.validate_num_chars(min, max) +} + +pub trait Chars { + fn validate_num_chars(&self, min: usize, max: usize) -> Result<(), Error>; +} + +impl Chars for T { + fn validate_num_chars(&self, min: usize, max: usize) -> Result<(), Error> { + super::check_len(self.num_chars(), min, max) + } +} + +impl Chars for Option { + fn validate_num_chars(&self, min: usize, max: usize) -> Result<(), Error> { + match self { + Some(v) => v.validate_num_chars(min, max), + None => Ok(()), + } + } +} + +pub trait HasChars { + fn num_chars(&self) -> usize; +} + +macro_rules! impl_via_chars { + ($(in <$lifetime:lifetime>)? $T:ty) => { + impl<$($lifetime)?> HasChars for $T { + fn num_chars(&self) -> usize { + self.chars().count() + } + } + }; +} + +impl_via_chars!(std::string::String); +impl_via_chars!(in<'a> &'a std::string::String); +impl_via_chars!(in<'a> &'a str); +impl_via_chars!(in<'a> std::borrow::Cow<'a, str>); +impl_via_chars!(std::rc::Rc); +impl_via_chars!(std::sync::Arc); +impl_via_chars!(std::boxed::Box); + +macro_rules! impl_via_len { + ($(in<$lifetime:lifetime>)? $T:ty) => { + impl<$($lifetime)?> HasChars for $T { + fn num_chars(&self) -> usize { + self.len() + } + } + }; +} + +impl_via_len!(in<'a> &'a [char]); +impl_via_len!(std::sync::Arc<[char]>); +impl_via_len!(std::rc::Rc<[char]>); +impl_via_len!(std::boxed::Box<[char]>); +impl_via_len!(std::vec::Vec); diff --git a/garde/src/rules/length/graphemes.rs b/garde/src/rules/length/graphemes.rs new file mode 100644 index 0000000..28cc839 --- /dev/null +++ b/garde/src/rules/length/graphemes.rs @@ -0,0 +1,52 @@ +//! Implemented by string-like types for which we can retrieve length in the number of graphemes. +//! +//! `garde` implementations of this trait use the [unicode-segmentation](https://crates.io/crates/unicode-segmentation) crate. + +use crate::error::Error; + +pub fn apply(v: &T, (min, max): (usize, usize)) -> Result<(), Error> { + v.validate_num_graphemes(min, max) +} + +pub trait Graphemes { + fn validate_num_graphemes(&self, min: usize, max: usize) -> Result<(), Error>; +} + +impl Graphemes for T { + fn validate_num_graphemes(&self, min: usize, max: usize) -> Result<(), Error> { + super::check_len(self.num_graphemes(), min, max) + } +} + +impl Graphemes for Option { + fn validate_num_graphemes(&self, min: usize, max: usize) -> Result<(), Error> { + match self { + Some(v) => v.validate_num_graphemes(min, max), + None => Ok(()), + } + } +} + +pub trait HasGraphemes { + fn num_graphemes(&self) -> usize; +} + +macro_rules! impl_str { + ($(in<$lifetime:lifetime>)? $T:ty) => { + impl<$($lifetime)?> HasGraphemes for $T { + fn num_graphemes(&self) -> usize { + use unicode_segmentation::UnicodeSegmentation; + + self.graphemes(true).count() + } + } + }; +} + +impl_str!(std::string::String); +impl_str!(in<'a> &'a std::string::String); +impl_str!(in<'a> &'a str); +impl_str!(in<'a> std::borrow::Cow<'a, str>); +impl_str!(std::rc::Rc); +impl_str!(std::sync::Arc); +impl_str!(std::boxed::Box); diff --git a/garde/src/rules/length/simple.rs b/garde/src/rules/length/simple.rs new file mode 100644 index 0000000..631cb83 --- /dev/null +++ b/garde/src/rules/length/simple.rs @@ -0,0 +1,114 @@ +//! Implemented by types which have a known length. +//! +//! The meaning of "length" depends on the type. +//! For example, the length of a `String` is defined as the number of _bytes_ it stores. + +use crate::error::Error; + +pub fn apply(v: &T, (min, max): (usize, usize)) -> Result<(), Error> { + v.validate_length(min, max) +} + +pub trait Simple { + fn validate_length(&self, min: usize, max: usize) -> Result<(), Error>; +} + +impl Simple for T { + fn validate_length(&self, min: usize, max: usize) -> Result<(), Error> { + super::check_len(self.length(), min, max) + } +} + +impl Simple for Option { + fn validate_length(&self, min: usize, max: usize) -> Result<(), Error> { + match self { + Some(v) => v.validate_length(min, max), + None => Ok(()), + } + } +} + +pub trait HasSimpleLength { + fn length(&self) -> usize; +} + +macro_rules! impl_via_bytes { + ($(in<$lifetime:lifetime>)? $T:ty) => { + impl<$($lifetime)?> HasSimpleLength for $T { + fn length(&self) -> usize { + use super::bytes::HasBytes as _; + self.num_bytes() + } + } + }; +} + +impl_via_bytes!(std::string::String); +impl_via_bytes!(in<'a> &'a std::string::String); +impl_via_bytes!(in<'a> &'a str); +impl_via_bytes!(in<'a> std::borrow::Cow<'a, str>); +impl_via_bytes!(std::rc::Rc); +impl_via_bytes!(std::sync::Arc); +impl_via_bytes!(std::boxed::Box); + +macro_rules! impl_via_len { + (in<$lifetime:lifetime, $($generic:ident),*> $T:ty) => { + impl<$lifetime, $($generic),*> HasSimpleLength for $T { + fn length(&self) -> usize { + self.len() + } + } + }; + (in<$($generic:ident),*> $T:ty) => { + impl<$($generic),*> HasSimpleLength for $T { + fn length(&self) -> usize { + self.len() + } + } + }; + (in<$lifetime:lifetime> $T:ty) => { + impl<$lifetime> HasSimpleLength for $T { + fn length(&self) -> usize { + self.len() + } + } + }; + ($T:ty) => { + impl HasSimpleLength for $T { + fn length(&self) -> usize { + self.len() + } + } + }; +} + +impl_via_len!(in Vec); +impl_via_len!(in<'a, T> &'a Vec); +impl_via_len!(in<'a, T> &'a [T]); + +impl Simple for [T; N] { + fn validate_length(&self, min: usize, max: usize) -> Result<(), Error> { + super::check_len(self.len(), min, max) + } +} + +impl<'a, const N: usize, T> Simple for &'a [T; N] { + fn validate_length(&self, min: usize, max: usize) -> Result<(), Error> { + super::check_len(self.len(), min, max) + } +} + +impl_via_len!(in std::collections::HashMap); +impl_via_len!(in std::collections::HashSet); +impl_via_len!(in std::collections::BTreeMap); +impl_via_len!(in std::collections::BTreeSet); +impl_via_len!(in std::collections::VecDeque); +impl_via_len!(in std::collections::BinaryHeap); +impl_via_len!(in std::collections::LinkedList); +impl_via_len!(in<'a, K, V, S> &'a std::collections::HashMap); +impl_via_len!(in<'a, T, S> &'a std::collections::HashSet); +impl_via_len!(in<'a, K, V> &'a std::collections::BTreeMap); +impl_via_len!(in<'a, T> &'a std::collections::BTreeSet); +impl_via_len!(in<'a, T> &'a std::collections::VecDeque); +impl_via_len!(in<'a, T> &'a std::collections::BinaryHeap); +impl_via_len!(in<'a, T> &'a std::collections::LinkedList); diff --git a/garde/src/rules/length/utf16.rs b/garde/src/rules/length/utf16.rs new file mode 100644 index 0000000..1d5a225 --- /dev/null +++ b/garde/src/rules/length/utf16.rs @@ -0,0 +1,48 @@ +//! Implemented by string-like types for which we can retrieve length in _UTF-16 code units_. + +use crate::error::Error; + +pub fn apply(v: &T, (min, max): (usize, usize)) -> Result<(), Error> { + v.validate_num_code_units(min, max) +} + +pub trait Utf16CodeUnits { + fn validate_num_code_units(&self, min: usize, max: usize) -> Result<(), Error>; +} + +impl Utf16CodeUnits for T { + fn validate_num_code_units(&self, min: usize, max: usize) -> Result<(), Error> { + super::check_len(self.num_code_units(), min, max) + } +} + +impl Utf16CodeUnits for Option { + fn validate_num_code_units(&self, min: usize, max: usize) -> Result<(), Error> { + match self { + Some(v) => v.validate_num_code_units(min, max), + None => Ok(()), + } + } +} + +pub trait HasUtf16CodeUnits { + fn num_code_units(&self) -> usize; +} + +macro_rules! impl_str { + ($(in<$lifetime:lifetime>)? $T:ty) => { + impl<$($lifetime)?> HasUtf16CodeUnits for $T { + fn num_code_units(&self) -> usize { + self.encode_utf16().count() + } + } + }; +} + +impl_str!(std::string::String); +impl_str!(in<'a> &'a std::string::String); +impl_str!(in<'a> &'a str); +impl_str!(in<'a> std::borrow::Cow<'a, str>); +impl_str!(std::rc::Rc); +impl_str!(std::sync::Arc); +impl_str!(std::boxed::Box); diff --git a/garde/src/rules/mod.rs b/garde/src/rules/mod.rs index 79dc2e4..a07d979 100644 --- a/garde/src/rules/mod.rs +++ b/garde/src/rules/mod.rs @@ -2,7 +2,6 @@ pub mod alphanumeric; pub mod ascii; -pub mod byte_length; pub mod contains; #[cfg(feature = "credit-card")] pub mod credit_card; diff --git a/garde/tests/rules/byte_length.rs b/garde/tests/rules/byte_length.rs deleted file mode 100644 index 139259a..0000000 --- a/garde/tests/rules/byte_length.rs +++ /dev/null @@ -1,99 +0,0 @@ -use super::util; - -const UWU: usize = 101; - -#[derive(Debug, garde::Validate)] -struct Test<'a> { - #[garde(byte_length(min = 10, max = UWU - 1))] - field: &'a str, - - #[garde(inner(length(min = 10, max = 100)))] - inner: &'a [&'a str], -} - -#[test] -fn byte_length_valid() { - util::check_ok(&[ - Test { - // 'a' * 10 - field: "aaaaaaaaaa", - inner: &["aaaaaaaaaa"], - }, - Test { - // 'a' * 100 - field: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - inner: &["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], - }, - Test { - // "๐Ÿ˜‚" = 4 bytes - // "๐Ÿ˜‚" * 25 = 100 bytes - field: "๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚", - inner: &["๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚"] - }, - ], &()) -} - -#[test] -fn byte_length_invalid() { - util::check_fail!(&[ - Test { - // 'a' * 9 - field: "aaaaaaaaa", - inner: &["aaaaaaaaa"], - }, - Test { - // 'a' * 101 - field: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", - inner: &["aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"], - }, - Test { - // "๐Ÿ˜‚" = 4 bytes - // 'a' * 1 + "๐Ÿ˜‚" * 25 = 101 bytes - field: "a๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚", - inner: &["a๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚๐Ÿ˜‚"], - }, - ], &()) -} - -#[derive(Debug, garde::Validate)] -struct Exact<'a> { - #[garde(byte_length(min = 4, max = 4))] - field: &'a str, - #[garde(inner(byte_length(min = 4, max = 4)))] - inner: &'a [&'a str], -} - -#[test] -fn exact_length_valid() { - util::check_ok( - &[Exact { - // '๐Ÿ˜‚' = 4 bytes - field: "๐Ÿ˜‚", - inner: &["๐Ÿ˜‚"], - }], - &(), - ) -} - -#[test] -fn exact_length_invalid() { - util::check_fail!( - &[ - Exact { - field: "", - inner: &[""] - }, - Exact { - // 'a' * 1 - field: "a", - inner: &["a"], - }, - Exact { - // '๐Ÿ˜‚' * 2 = 8 - field: "๐Ÿ˜‚๐Ÿ˜‚", - inner: &["๐Ÿ˜‚๐Ÿ˜‚"] - }, - ], - &() - ) -} diff --git a/garde/tests/rules/length.rs b/garde/tests/rules/length.rs index d3245fe..85a2110 100644 --- a/garde/tests/rules/length.rs +++ b/garde/tests/rules/length.rs @@ -82,3 +82,52 @@ fn exact_length_invalid() { &() ) } + +#[derive(Debug, garde::Validate)] +struct SpecialLengthTest<'a> { + #[garde(length(simple, max = 1))] + simple: &'a str, + #[garde(length(bytes, max = 1))] + bytes: &'a str, + #[garde(length(chars, max = 1))] + chars: &'a str, + #[garde(length(graphemes, max = 1))] + graphemes: &'a str, + #[garde(length(utf16, max = 1))] + utf16: &'a str, + + #[garde(length(bytes, max = 4), length(graphemes, max = 1))] + multi: &'a str, +} + +#[test] +fn char_length_valid() { + util::check_ok( + &[SpecialLengthTest { + simple: "a", + bytes: "a", + chars: "รก", + graphemes: "รก", + utf16: "รก", + + multi: "๐Ÿ˜‚", // 4 bytes, 1 grapheme + }], + &(), + ) +} + +#[test] +fn char_length_invalid() { + util::check_fail!( + &[SpecialLengthTest { + simple: "ab", // 2 bytes + bytes: "ab", // 2 bytes + chars: "yฬ†", // 2 USVs + graphemes: "รกรก", // 2 graphemes + utf16: "๐Ÿ˜‚", // 2 units + + multi: "รกรก", // 4 bytes, 2 graphemes + }], + &() + ) +} diff --git a/garde/tests/rules/mod.rs b/garde/tests/rules/mod.rs index 4e968e6..fb57932 100644 --- a/garde/tests/rules/mod.rs +++ b/garde/tests/rules/mod.rs @@ -1,7 +1,6 @@ mod allow_unvalidated; mod alphanumeric; mod ascii; -mod byte_length; mod contains; mod credit_card; mod custom; diff --git a/garde/tests/rules/option.rs b/garde/tests/rules/option.rs index 0c5500c..820a5c0 100644 --- a/garde/tests/rules/option.rs +++ b/garde/tests/rules/option.rs @@ -6,10 +6,8 @@ struct Test<'a> { alphanumeric: Option<&'a str>, #[garde(ascii)] ascii: Option<&'a str>, - #[garde(byte_length(min = 1))] - byte_length_min1_str: Option<&'a str>, - #[garde(byte_length(min = 1))] - byte_length_min1_u8_slice: Option<&'a [u8]>, + #[garde(length(min = 1))] + length_min1_u8_slice: Option<&'a [u8]>, #[garde(contains("a"))] contains_a: Option<&'a str>, #[garde(credit_card)] @@ -42,8 +40,7 @@ fn option_valid() { &[Test { alphanumeric: Some("a"), ascii: Some("a"), - byte_length_min1_str: Some("a"), - byte_length_min1_u8_slice: Some(&[0]), + length_min1_u8_slice: Some(&[0]), contains_a: Some("a"), credit_card: Some("4539571147647251"), email: Some("test@mail.com"), @@ -68,8 +65,7 @@ fn option_invalid() { Test { alphanumeric: Some("๐Ÿ˜‚"), ascii: Some("๐Ÿ˜‚"), - byte_length_min1_str: Some(""), - byte_length_min1_u8_slice: Some(&[]), + length_min1_u8_slice: Some(&[]), contains_a: Some("๐Ÿ˜‚"), credit_card: Some("๐Ÿ˜‚"), email: Some("๐Ÿ˜‚"), @@ -86,8 +82,7 @@ fn option_invalid() { Test { alphanumeric: None, ascii: None, - byte_length_min1_str: None, - byte_length_min1_u8_slice: None, + length_min1_u8_slice: None, contains_a: None, credit_card: None, email: None, diff --git a/garde/tests/rules/select.rs b/garde/tests/rules/select.rs index 9677107..40b4fa8 100644 --- a/garde/tests/rules/select.rs +++ b/garde/tests/rules/select.rs @@ -8,7 +8,7 @@ pub struct UserIdentifier { #[derive(Validate)] pub struct UserRole { - #[garde(ascii, byte_length(min = 10))] + #[garde(ascii, length(min = 10))] pub name: String, #[garde(dive)] pub identifiers: Vec, @@ -32,6 +32,6 @@ fn select_macro() { let errors: Vec = garde::select!(report, name) .map(|e| e.to_string()) .collect(); - assert_eq!(errors, ["not ascii", "byte length is lower than 10"]) + assert_eq!(errors, ["not ascii", "length is lower than 10"]) } } diff --git a/garde/tests/rules/snapshots/rules__rules__length__char_length_invalid.snap b/garde/tests/rules/snapshots/rules__rules__length__char_length_invalid.snap new file mode 100644 index 0000000..c0860b3 --- /dev/null +++ b/garde/tests/rules/snapshots/rules__rules__length__char_length_invalid.snap @@ -0,0 +1,20 @@ +--- +source: garde/tests/./rules/length.rs +expression: snapshot +--- +SpecialLengthTest { + simple: "ab", + bytes: "ab", + chars: "y\u{306}", + graphemes: "รกรก", + utf16: "๐Ÿ˜‚", + multi: "รกรก", +} +bytes: length is greater than 1 +chars: length is greater than 1 +graphemes: length is greater than 1 +multi: length is greater than 1 +simple: length is greater than 1 +utf16: length is greater than 1 + + diff --git a/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap b/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap index 52a4962..08200c2 100644 --- a/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap +++ b/garde/tests/rules/snapshots/rules__rules__option__option_invalid.snap @@ -9,10 +9,7 @@ Test { ascii: Some( "๐Ÿ˜‚", ), - byte_length_min1_str: Some( - "", - ), - byte_length_min1_u8_slice: Some( + length_min1_u8_slice: Some( [], ), contains_a: Some( @@ -52,13 +49,12 @@ Test { } alphanumeric: not alphanumeric ascii: not ascii -byte_length_min1_str: byte length is lower than 1 -byte_length_min1_u8_slice: byte length is lower than 1 contains_a: does not contain "a" credit_card: not a valid credit card number: invalid format email: not a valid email: value is missing `@` ip: not a valid IP address length_min1: length is lower than 1 +length_min1_u8_slice: length is lower than 1 pat_a_or_b: does not match pattern /a|b/ phone_number: not a valid phone number: not a number prefix_a: value does not begin with "a" @@ -70,8 +66,7 @@ url: not a valid url: relative URL without a base Test { alphanumeric: None, ascii: None, - byte_length_min1_str: None, - byte_length_min1_u8_slice: None, + length_min1_u8_slice: None, contains_a: None, credit_card: None, email: None, diff --git a/garde/tests/ui/compile-fail/byte_length_bad_min.rs b/garde/tests/ui/compile-fail/byte_length_bad_min.rs deleted file mode 100644 index adc8e39..0000000 --- a/garde/tests/ui/compile-fail/byte_length_bad_min.rs +++ /dev/null @@ -1,7 +0,0 @@ -#[derive(garde::Validate)] -struct Test<'a> { - #[garde(byte_length(min = 100, max = 10))] - field: &'a str, -} - -fn main() {} diff --git a/garde/tests/ui/compile-fail/byte_length_bad_min.stderr b/garde/tests/ui/compile-fail/byte_length_bad_min.stderr deleted file mode 100644 index 17eb62c..0000000 --- a/garde/tests/ui/compile-fail/byte_length_bad_min.stderr +++ /dev/null @@ -1,5 +0,0 @@ -error: `min` must be lower than or equal to `max` - --> tests/ui/compile-fail/byte_length_bad_min.rs:3:25 - | -3 | #[garde(byte_length(min = 100, max = 10))] - | ^^^ diff --git a/garde/tests/ui/compile-fail/length_bad_arg.rs b/garde/tests/ui/compile-fail/length_bad_arg.rs new file mode 100644 index 0000000..d5e573a --- /dev/null +++ b/garde/tests/ui/compile-fail/length_bad_arg.rs @@ -0,0 +1,9 @@ +#[derive(garde::Validate)] +struct Test<'a> { + #[garde(length(min = 10, invalid_arg, max = 50))] + field: &'a str, + #[garde(length(min = 10, invalid_arg = 10, max = 50))] + field2: &'a str, +} + +fn main() {} diff --git a/garde/tests/ui/compile-fail/length_bad_arg.stderr b/garde/tests/ui/compile-fail/length_bad_arg.stderr new file mode 100644 index 0000000..1253909 --- /dev/null +++ b/garde/tests/ui/compile-fail/length_bad_arg.stderr @@ -0,0 +1,11 @@ +error: invalid argument + --> tests/ui/compile-fail/length_bad_arg.rs + | + | #[garde(length(min = 10, invalid_arg, max = 50))] + | ^^^^^^^^^^^ + +error: invalid argument + --> tests/ui/compile-fail/length_bad_arg.rs + | + | #[garde(length(min = 10, invalid_arg = 10, max = 50))] + | ^^^^^^^^^^^ diff --git a/garde/tests/ui/compile-pass/byte_length.rs b/garde/tests/ui/compile-pass/byte_length.rs deleted file mode 100644 index 9960063..0000000 --- a/garde/tests/ui/compile-pass/byte_length.rs +++ /dev/null @@ -1,11 +0,0 @@ -#[derive(garde::Validate)] -struct Test<'a> { - #[garde(byte_length(min = 10, max = 100))] - field: &'a str, - #[garde(byte_length(min = 10, max = 10))] - field2: &'a str, - #[garde(inner(byte_length(min = 10, max = 10)))] - inner: &'a [&'a str], -} - -fn main() {} diff --git a/garde/tests/ui/compile-pass/custom.rs b/garde/tests/ui/compile-pass/custom.rs index 16a4a98..7045631 100644 --- a/garde/tests/ui/compile-pass/custom.rs +++ b/garde/tests/ui/compile-pass/custom.rs @@ -15,11 +15,11 @@ fn custom_validate_fn(_: &str, _: &()) -> Result<(), garde::Error> { } #[repr(transparent)] -pub struct MyString(pub String); +pub struct MyString(String); -impl garde::rules::length::HasLength for MyString { +impl garde::rules::length::HasSimpleLength for MyString { fn length(&self) -> usize { - self.0.chars().count() + self.0.len() } } diff --git a/garde/tests/ui/compile-pass/length.rs b/garde/tests/ui/compile-pass/length.rs index e922cb2..03a22de 100644 --- a/garde/tests/ui/compile-pass/length.rs +++ b/garde/tests/ui/compile-pass/length.rs @@ -8,6 +8,17 @@ struct Test<'a> { inner: &'a [&'a str], #[garde(inner(length(min = 10, max = 10)))] inner2: &'a [&'a str], + + #[garde(length(simple, min = 1, max = 1))] + simple: &'a str, + #[garde(length(bytes, min = 1, max = 1))] + bytes: &'a str, + #[garde(length(chars, min = 1, max = 1))] + chars: &'a str, + #[garde(length(graphemes, min = 1, max = 1))] + graphemes: &'a str, + #[garde(length(utf16, min = 1, max = 1))] + utf16: &'a str, } fn main() {} diff --git a/garde_derive/src/check.rs b/garde_derive/src/check.rs index a831e9b..cd417c4 100644 --- a/garde_derive/src/check.rs +++ b/garde_derive/src/check.rs @@ -5,6 +5,7 @@ use syn::parse_quote; use syn::spanned::Spanned; use crate::model; +use crate::model::LengthMode; use crate::util::{default_ctx_name, MaybeFoldError}; pub fn check(input: model::Input) -> syn::Result { @@ -301,7 +302,6 @@ fn check_rule( rule_set: &mut model::RuleSet, is_inner: bool, ) -> syn::Result<()> { - // TODO: can this be simplified via a macro? there's a ton of duplicated code macro_rules! apply { ($is_inner:expr, $field:ident, $name:ident, $value:expr, $span:expr) => {{ if $is_inner { @@ -321,10 +321,10 @@ fn check_rule( } }}; - ($rule_set:ident, $rule:ident($($inner:expr)?), $span:expr) => {{ + ($rule:ident($($inner:expr)?), $span:expr) => {{ let rule = model::ValidateRule::$rule$(($inner))?; let name = rule.name(); - if !$rule_set.rules.insert(rule) { + if !rule_set.rules.insert(rule) { return Err(syn::Error::new($span, format!("duplicate rule `{name}`"))); } }}; @@ -339,23 +339,31 @@ fn check_rule( Code(code) => apply!(is_inner, field, code, code.value, span), Dive => apply!(is_inner, field, dive, span, span), Custom(custom) => rule_set.custom_rules.push(custom), - Required => apply!(rule_set, Required(), span), - Ascii => apply!(rule_set, Ascii(), span), - Alphanumeric => apply!(rule_set, Alphanumeric(), span), - Email => apply!(rule_set, Email(), span), - Url => apply!(rule_set, Url(), span), - Ip => apply!(rule_set, Ip(), span), - IpV4 => apply!(rule_set, IpV4(), span), - IpV6 => apply!(rule_set, IpV6(), span), - CreditCard => apply!(rule_set, CreditCard(), span), - PhoneNumber => apply!(rule_set, PhoneNumber(), span), - Length(v) => apply!(rule_set, Length(check_range_generic(v)?), span), - ByteLength(v) => apply!(rule_set, ByteLength(check_range_generic(v)?), span), - Range(v) => apply!(rule_set, Range(check_range_not_ord(v)?), span), - Contains(v) => apply!(rule_set, Contains(v), span), - Prefix(v) => apply!(rule_set, Prefix(v), span), - Suffix(v) => apply!(rule_set, Suffix(v), span), - Pattern(v) => apply!(rule_set, Pattern(check_regex(v)?), span), + Required => apply!(Required(), span), + Ascii => apply!(Ascii(), span), + Alphanumeric => apply!(Alphanumeric(), span), + Email => apply!(Email(), span), + Url => apply!(Url(), span), + Ip => apply!(Ip(), span), + IpV4 => apply!(IpV4(), span), + IpV6 => apply!(IpV6(), span), + CreditCard => apply!(CreditCard(), span), + PhoneNumber => apply!(PhoneNumber(), span), + Length(v) => { + let range = check_range_generic(v.range)?; + match v.mode { + LengthMode::Simple => apply!(LengthSimple(range), span), + LengthMode::Bytes => apply!(LengthBytes(range), span), + LengthMode::Chars => apply!(LengthChars(range), span), + LengthMode::Graphemes => apply!(LengthGraphemes(range), span), + LengthMode::Utf16 => apply!(LengthUtf16(range), span), + } + } + Range(v) => apply!(Range(check_range_not_ord(v)?), span), + Contains(v) => apply!(Contains(v), span), + Prefix(v) => apply!(Prefix(v), span), + Suffix(v) => apply!(Suffix(v), span), + Pattern(v) => apply!(Pattern(check_regex(v)?), span), Inner(v) => { if rule_set.inner.is_none() { rule_set.inner = Some(Box::new(model::RuleSet::empty())); diff --git a/garde_derive/src/emit.rs b/garde_derive/src/emit.rs index 8522343..e0c38dd 100644 --- a/garde_derive/src/emit.rs +++ b/garde_derive/src/emit.rs @@ -1,4 +1,5 @@ use std::cell::RefCell; +use std::str::FromStr as _; use proc_macro2::{Ident, TokenStream as TokenStream2}; use quote::{format_ident, quote, quote_spanned, ToTokens}; @@ -238,7 +239,7 @@ impl<'a> ToTokens for Rules<'a> { } for rule in rule_set.rules.iter() { - let name = format_ident!("{}", rule.name()); + let name = TokenStream2::from_str(rule.name()).unwrap(); use model::ValidateRule::*; let args = match rule { Ascii | Alphanumeric | Email | Url | CreditCard | PhoneNumber | Required => { @@ -253,10 +254,20 @@ impl<'a> ToTokens for Rules<'a> { IpV6 => { quote!((::garde::rules::ip::IpKind::V6,)) } - Length(range) | ByteLength(range) => match range { - model::ValidateRange::GreaterThan(min) => quote!((#min, usize::MAX)), - model::ValidateRange::LowerThan(max) => quote!((0usize, #max)), - model::ValidateRange::Between(min, max) => quote!((#min, #max)), + LengthSimple(range) + | LengthBytes(range) + | LengthChars(range) + | LengthGraphemes(range) + | LengthUtf16(range) => match range { + model::ValidateRange::GreaterThan(min) => { + quote!((#min, usize::MAX)) + } + model::ValidateRange::LowerThan(max) => { + quote!((0usize, #max)) + } + model::ValidateRange::Between(min, max) => { + quote!((#min, #max)) + } }, Range(range) => match range { model::ValidateRange::GreaterThan(min) => quote!((Some(#min), None)), diff --git a/garde_derive/src/model.rs b/garde_derive/src/model.rs index 1ca0cbc..565f7ad 100644 --- a/garde_derive/src/model.rs +++ b/garde_derive/src/model.rs @@ -88,8 +88,7 @@ pub enum RawRuleKind { IpV6, CreditCard, PhoneNumber, - Length(Range>), - ByteLength(Range>), + Length(RawLength), Range(Range), Contains(Expr), Prefix(Expr), @@ -99,6 +98,21 @@ pub enum RawRuleKind { Inner(List), } +pub struct RawLength { + pub mode: LengthMode, + pub range: Range>, +} + +#[derive(Clone, Copy, Default)] +pub enum LengthMode { + #[default] + Simple, + Bytes, + Chars, + Graphemes, + Utf16, +} + pub enum Either { Left(L), Right(R), @@ -218,8 +232,11 @@ pub enum ValidateRule { IpV6, CreditCard, PhoneNumber, - Length(ValidateRange>), - ByteLength(ValidateRange>), + LengthSimple(LengthRange), + LengthBytes(LengthRange), + LengthChars(LengthRange), + LengthGraphemes(LengthRange), + LengthUtf16(LengthRange), Range(ValidateRange), Contains(Expr), Prefix(Expr), @@ -227,6 +244,8 @@ pub enum ValidateRule { Pattern(ValidatePattern), } +type LengthRange = ValidateRange>; + impl ValidateRule { pub fn name(&self) -> &'static str { match self { @@ -240,9 +259,12 @@ impl ValidateRule { ValidateRule::IpV6 => "ip", ValidateRule::CreditCard => "credit_card", ValidateRule::PhoneNumber => "phone_number", - ValidateRule::Length { .. } => "length", - ValidateRule::ByteLength { .. } => "byte_length", - ValidateRule::Range { .. } => "range", + ValidateRule::LengthSimple(_) => "length::simple", + ValidateRule::LengthBytes(_) => "length::bytes", + ValidateRule::LengthChars(_) => "length::chars", + ValidateRule::LengthGraphemes(_) => "length::graphemes", + ValidateRule::LengthUtf16(_) => "length::utf16", + ValidateRule::Range(_) => "range", ValidateRule::Contains(_) => "contains", ValidateRule::Prefix(_) => "prefix", ValidateRule::Suffix(_) => "suffix", diff --git a/garde_derive/src/syntax.rs b/garde_derive/src/syntax.rs index b23f8b7..61b6822 100644 --- a/garde_derive/src/syntax.rs +++ b/garde_derive/src/syntax.rs @@ -289,7 +289,6 @@ impl Parse for model::RawRule { "credit_card" => CreditCard, "phone_number" => PhoneNumber, "length" => Length(content), - "byte_length" => ByteLength(content), "range" => Range(content), "contains" => Contains(content), "prefix" => Prefix(content), @@ -331,6 +330,103 @@ impl Parse for model::Message { } } +impl Parse for model::RawLength { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let span = input.span(); + + let args = + Punctuated::, Token![,]>::parse_terminated(input)?; + + let mut error = None; + + let mut mode = None; + let mut min = None; + let mut max = None; + + for arg in args { + let arg = match arg { + ContinueOnFail::Ok(arg) => arg, + ContinueOnFail::Err(e) => { + error.maybe_fold(e); + continue; + } + }; + match arg { + RawLengthArgument::Min(span, v) => { + if min.is_some() { + error.maybe_fold(syn::Error::new(span, "duplicate argument")); + continue; + } + min = Some(v); + } + RawLengthArgument::Max(span, v) => { + if max.is_some() { + error.maybe_fold(syn::Error::new(span, "duplicate argument")); + continue; + } + max = Some(v); + } + RawLengthArgument::Mode(span, v) => { + if mode.is_some() { + error.maybe_fold(syn::Error::new(span, "duplicate argument")); + continue; + } + mode = Some(v); + } + } + } + + if let Some(error) = error { + return Err(error); + } + + Ok(model::RawLength { + mode: mode.unwrap_or_default(), + range: model::Range { span, min, max }, + }) + } +} + +enum RawLengthArgument { + Min(Span, model::Either), + Max(Span, model::Either), + Mode(Span, model::LengthMode), +} + +impl Parse for RawLengthArgument { + fn parse(input: syn::parse::ParseStream) -> syn::Result { + let ident = Ident::parse_any(input)?; + let span = ident.span(); + let v = match ident.to_string().as_str() { + "simple" => RawLengthArgument::Mode(span, model::LengthMode::Simple), + "bytes" => RawLengthArgument::Mode(span, model::LengthMode::Bytes), + "chars" => RawLengthArgument::Mode(span, model::LengthMode::Chars), + "graphemes" => RawLengthArgument::Mode(span, model::LengthMode::Graphemes), + "utf16" => RawLengthArgument::Mode(span, model::LengthMode::Utf16), + "min" => { + let _ = input.parse::()?; + let v = input.parse::()?; + RawLengthArgument::Min(span, FromExpr::from_expr(v)?) + } + "max" => { + let _ = input.parse::()?; + let v = input.parse::()?; + RawLengthArgument::Max(span, FromExpr::from_expr(v)?) + } + _ => { + if input.peek(Token![=]) { + let _ = input.parse::()?; + } + if !input.peek(Token![,]) { + let _ = input.parse::()?; + } + return Err(syn::Error::new(span, "invalid argument")); + } + }; + Ok(v) + } +} + impl Parse for model::Range where T: FromExpr, @@ -338,8 +434,7 @@ where fn parse(input: syn::parse::ParseStream) -> syn::Result { let span = input.span(); - let pairs = - syn::punctuated::Punctuated::::parse_terminated(input)?; + let pairs = Punctuated::::parse_terminated(input)?; let mut error = None;