Skip to content

Commit

Permalink
Merge pull request #168 from greyblake/custom-error
Browse files Browse the repository at this point in the history
Add Validation::Custom variant
  • Loading branch information
greyblake authored Aug 24, 2024
2 parents f9aa857 + 9be4ce6 commit bc00fa3
Show file tree
Hide file tree
Showing 37 changed files with 768 additions and 143 deletions.
7 changes: 4 additions & 3 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
### v0.x.x - 2024-xx-xx

* [BREAKING] Replace lazy_static with [`std::sync::LazyLock`](https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html) for regex validation (requires Rust 1.80). This change is potentially is a breaking change, because the code that uses regex may stop compiling on the older versions of Rust, since it generates that relies on `LazyLock`.
### v0.5.0 - 2024-xx-xx

- **[FEATURE]** Added support for custom error types and validation functions via the `error` and `with` attributes.
- **[BREAKING]** Replaced `lazy_static` with [`std::sync::LazyLock`](https://doc.rust-lang.org/stable/std/sync/struct.LazyLock.html) for regex validation. This requires Rust 1.80 or higher and may cause compilation issues on older Rust versions due to the use of `std::sync::LazyLock`. If upgrading Rust isn't an option, you can still use `lazy_static` explicitly as a workaround.
- **[BREAKING]** The fallible `::new()` constructor has been fully replaced by `::try_new()`.

### v0.4.3 - 2024-07-06

Expand Down
29 changes: 29 additions & 0 deletions Cargo.lock

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

4 changes: 3 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,7 @@ members = [
"examples/serde_complex",
"examples/string_bounded_len",
"examples/string_regex_email",
"examples/string_arbitrary", "examples/any_generics",
"examples/string_arbitrary",
"examples/any_generics",
"examples/custom_error",
]
74 changes: 57 additions & 17 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Nutype is a proc macro that allows adding extra constraints like _sanitization_

* [Quick start](#quick-start)
* [Inner types](#inner-types) ([String](#string) | [Integer](#integer) | [Float](#float) | [Other](#other-inner-types-and-generics))
* [Custom](#custom-sanitizers) ([sanitizers](#custom-sanitizers) | [validators](#custom-validators))
* [Custom](#custom-sanitizers) ([sanitizers](#custom-sanitizers) | [validators](#custom-validators) | [errors](#custom-validation-with-a-custom-error-type))
* [Recipes](#recipes)
* [Breaking constraints with new_unchecked](#breaking-constraints-with-new_unchecked)
* [Feature Flags](#feature-flags)
Expand Down Expand Up @@ -84,7 +84,7 @@ At the moment the string inner type supports only `String` (owned) type.
| `trim` | Removes leading and trailing whitespaces | `trim` |
| `lowercase` | Converts the string to lowercase | `lowercase` |
| `uppercase` | Converts the string to uppercase | `uppercase` |
| `with` | Custom sanitizer. A function or closure that receives `String` and returns `String` | `with = \|mut s: String\| { s.truncate(5); s }` |
| `with` | Custom sanitizer. A function or closure that receives `String` and returns `String` | `with = \|mut s: String\| ( s.truncate(5); s )` |

### String validators

Expand All @@ -95,6 +95,7 @@ At the moment the string inner type supports only `String` (owned) type.
| `not_empty` | Rejects an empty string | `NotEmptyViolated` | `not_empty` |
| `regex` | Validates format with a regex. Requires `regex` feature. | `RegexViolated` | `regex = "^[0-9]{7}$"` or `regex = ID_REGEX` |
| `predicate` | Custom validator. A function or closure that receives `&str` and returns `bool` | `PredicateViolated` | `predicate = \|s: &str\| s.contains('@')` |
| `with` | Custom validator with a custom error | N/A | (see example below) |


#### Regex validation
Expand Down Expand Up @@ -170,13 +171,14 @@ The integer inner types are: `u8`, `u16`,`u32`, `u64`, `u128`, `i8`, `i16`, `i32

### Integer validators

| Validator | Description | Error variant | Example |
| ------------------- | --------------------- | ------------------------- | ------------------------------------ |
| `less` | Exclusive upper bound | `LessViolated` | `less = 100` |
| `less_or_equal` | Inclusive upper bound | `LessOrEqualViolated` | `less_or_equal = 99` |
| `greater` | Exclusive lower bound | `GreaterViolated` | `greater = 17` |
| `greater_or_equal` | Inclusive lower bound | `GreaterOrEqualViolated` | `greater_or_equal = 18` |
| `predicate` | Custom predicate | `PredicateViolated` | `predicate = \|num\| num % 2 == 0` |
| Validator | Description | Error variant | Example |
| ------------------- | ------------------------------------- | ------------------------- | ------------------------------------ |
| `less` | Exclusive upper bound | `LessViolated` | `less = 100` |
| `less_or_equal` | Inclusive upper bound | `LessOrEqualViolated` | `less_or_equal = 99` |
| `greater` | Exclusive lower bound | `GreaterViolated` | `greater = 17` |
| `greater_or_equal` | Inclusive lower bound | `GreaterOrEqualViolated` | `greater_or_equal = 18` |
| `predicate` | Custom predicate | `PredicateViolated` | `predicate = \|num\| num % 2 == 0` |
| `with` | Custom validator with a custom error | N/A | (see example below) |

### Integer derivable traits

Expand All @@ -197,14 +199,15 @@ The float inner types are: `f32`, `f64`.

### Float validators

| Validator | Description | Error variant | Example |
| ------------------ | -------------------------------- | --------------------- | ----------------------------------- |
| `less` | Exclusive upper bound | `LessViolated` | `less = 100.0` |
| `less_or_equal` | Inclusive upper bound | `LessOrEqualViolated` | `less_or_equal = 100.0` |
| `greater` | Exclusive lower bound | `GreaterViolated` | `greater = 0.0` |
| `greater_or_equal` | Inclusive lower bound | `GreaterOrEqualViolated` | `greater_or_equal = 0.0` |
| `finite` | Check against NaN and infinity | `FiniteViolated` | `finite` |
| `predicate` | Custom predicate | `PredicateViolated` | `predicate = \|val\| val != 50.0` |
| Validator | Description | Error variant | Example |
| ------------------ | ------------------------------------ | ------------------------ | ----------------------------------- |
| `less` | Exclusive upper bound | `LessViolated` | `less = 100.0` |
| `less_or_equal` | Inclusive upper bound | `LessOrEqualViolated` | `less_or_equal = 100.0` |
| `greater` | Exclusive lower bound | `GreaterViolated` | `greater = 0.0` |
| `greater_or_equal` | Inclusive lower bound | `GreaterOrEqualViolated` | `greater_or_equal = 0.0` |
| `finite` | Check against NaN and infinity | `FiniteViolated` | `finite` |
| `predicate` | Custom predicate | `PredicateViolated` | `predicate = \|val\| val != 50.0` |
| `with` | Custom validator with a custom error | N/A | (see example below) |

### Float derivable traits

Expand Down Expand Up @@ -305,6 +308,43 @@ fn is_valid_name(name: &str) -> bool {
}
```

## Custom validation with a custom error type

To define your own error type and implement custom validation logic, you can combine the `with` and `error` attributes:

```rust
// Define a custom error type for validation failures.
// Although it's best practice to implement `std::error::Error` for custom error types,
// we are omitting that for simplicity here.
#[derive(Debug, PartialEq)]
enum NameError {
TooShort,
TooLong,
}

// Define a custom validation function for `Name`.
// The function returns `Result<(), NameError>`, where `Ok(())` indicates a valid name,
// and `Err(NameError)` represents a specific validation failure.
fn validate_name(name: &str) -> Result<(), NameError> {
if name.len() < 3 {
Err(NameError::TooShort)
} else if name.len() > 10 {
Err(NameError::TooLong)
} else {
Ok(())
}
}

// Define a newtype `Name` with custom validation logic and custom error.
#[nutype(
validate(with = validate_name, error = NameError),
derive(Debug, PartialEq),
)]
struct Name(String);
```

It's important to ensure that the type specified in the `error` attribute matches the error type returned by the validation function.

## Recipes

### Derive `Default`
Expand Down
24 changes: 14 additions & 10 deletions dummy/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,31 +1,35 @@
/*
use nutype::nutype;

// Validation function
fn validate_name(name: &str) -> Result<(), NameError> {
if name.len() < 3 {
Err(NameError::TooShort)
} else if name.len() > 20 {
} else if name.len() > 10 {
Err(NameError::TooLong)
} else {
Ok(())
}
}

// Name validation error
#[derive(Debug)]
#[derive(Debug, PartialEq)]
enum NameError {
TooShort,
TooLong,
}

// Variant 1: with and error
#[nutype(
sanitize(trim),
validate(with = validate_name, error = NameError),
derive(Debug, AsRef, PartialEq, Deref),
derive(Debug, AsRef, PartialEq),
)]
struct Name(String);
*/

fn main() {}
fn main() {
let name = Name::try_new("John").unwrap();
assert_eq!(name.as_ref(), "John");

assert_eq!(
Name::try_new("JohnJohnJohnJohnJohn"),
Err(NameError::TooLong)
);

assert_eq!(Name::try_new("Jo"), Err(NameError::TooShort));
}
8 changes: 8 additions & 0 deletions examples/custom_error/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[package]
name = "custom_error"
version = "0.1.0"
edition = "2021"

[dependencies]
nutype = { path = "../../nutype" }
thiserror = "1.0.63"
43 changes: 43 additions & 0 deletions examples/custom_error/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
use nutype::nutype;
use thiserror::Error;

#[nutype(
validate(with = validate_positively_odd, error = PositivelyOddError),
derive(Debug, FromStr),
)]
struct PositivelyOdd(i32);

#[derive(Error, Debug, PartialEq)]
enum PositivelyOddError {
#[error("The value is negative.")]
Negative,

#[error("The value is even.")]
Even,
}

fn validate_positively_odd(value: &i32) -> Result<(), PositivelyOddError> {
if *value < 0 {
return Err(PositivelyOddError::Negative);
}

if *value % 2 == 0 {
return Err(PositivelyOddError::Even);
}

Ok(())
}

fn main() {
let err = PositivelyOdd::try_new(2).unwrap_err();
assert_eq!(err, PositivelyOddError::Even);

let podd: PositivelyOdd = PositivelyOdd::try_new(3).unwrap();
assert_eq!(podd.into_inner(), 3);

let err: PositivelyOddParseError = "-3".parse::<PositivelyOdd>().unwrap_err();
assert!(matches!(
err,
PositivelyOddParseError::Validate(PositivelyOddError::Negative)
));
}
Loading

0 comments on commit bc00fa3

Please sign in to comment.