diff --git a/CHANGELOG.md b/CHANGELOG.md index d93bf51..db8194e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/Cargo.lock b/Cargo.lock index b9589b4..a586077 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -107,6 +107,14 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "custom_error" +version = "0.1.0" +dependencies = [ + "nutype", + "thiserror", +] + [[package]] name = "derive_arbitrary" version = "1.3.2" @@ -570,9 +578,30 @@ dependencies = [ "schemars", "serde", "serde_json", + "thiserror", "trybuild", ] +[[package]] +name = "thiserror" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "trybuild" version = "1.0.80" diff --git a/Cargo.toml b/Cargo.toml index 8a3b4bc..e36cb55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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", ] diff --git a/README.md b/README.md index e5b620a..642f4c7 100644 --- a/README.md +++ b/README.md @@ -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) @@ -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 @@ -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 @@ -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 @@ -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 @@ -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` diff --git a/dummy/src/main.rs b/dummy/src/main.rs index e775404..8f213f8 100644 --- a/dummy/src/main.rs +++ b/dummy/src/main.rs @@ -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)); +} diff --git a/examples/custom_error/Cargo.toml b/examples/custom_error/Cargo.toml new file mode 100644 index 0000000..8911fda --- /dev/null +++ b/examples/custom_error/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "custom_error" +version = "0.1.0" +edition = "2021" + +[dependencies] +nutype = { path = "../../nutype" } +thiserror = "1.0.63" diff --git a/examples/custom_error/src/main.rs b/examples/custom_error/src/main.rs new file mode 100644 index 0000000..7884d37 --- /dev/null +++ b/examples/custom_error/src/main.rs @@ -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::().unwrap_err(); + assert!(matches!( + err, + PositivelyOddParseError::Validate(PositivelyOddError::Negative) + )); +} diff --git a/nutype/src/lib.rs b/nutype/src/lib.rs index e4008d0..aba8198 100644 --- a/nutype/src/lib.rs +++ b/nutype/src/lib.rs @@ -58,7 +58,7 @@ //! //! Here are some other examples of what you can do with `nutype`. //! -//! You can skip `sanitize` and use a custom validator `with`: +//! You can skip `sanitize` and use a custom validator `predicate`: //! //! ``` //! use nutype::nutype; @@ -109,6 +109,7 @@ //! | `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 //! @@ -205,13 +206,14 @@ //! //! ### 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 //! @@ -232,14 +234,15 @@ //! //! ### 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 //! @@ -263,7 +266,7 @@ //! ## Other inner types and generics //! //! For any other type it is possible to define custom sanitizers with `with` and custom -//! validations with `predicate`: +//! validations with `predicate` or `with`: //! //! ``` //! use nutype::nutype; @@ -333,7 +336,7 @@ //! assert_eq!(city.into_inner(), "Old York"); //! ``` //! -//! ## Custom validators +//! ## Custom validation with predicate //! //! In similar fashion it's possible to define custom validators, but a validation function receives a reference and returns `bool`. //! Think of it as a predicate. @@ -352,6 +355,47 @@ //! fn main() { } //! ``` //! +//! ## 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: +//! +//! ``` +//! # mod wrapper_module { +//! use nutype::nutype; +//! +//! // 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` diff --git a/nutype_macros/src/any/parse.rs b/nutype_macros/src/any/parse.rs index b41187f..6fdee43 100644 --- a/nutype_macros/src/any/parse.rs +++ b/nutype_macros/src/any/parse.rs @@ -24,16 +24,14 @@ pub fn parse_attributes( let ParseableAttributes { sanitizers, - validators, + validation, new_unchecked, default, derive_traits, - error_type_name, } = attrs; let raw_guard = AnyRawGuard { sanitizers, - validators, - error_type_name, + validation, }; let guard = validate_any_guard(raw_guard, type_name)?; Ok(Attributes { diff --git a/nutype_macros/src/common/gen/mod.rs b/nutype_macros/src/common/gen/mod.rs index 5dae0c3..01da127 100644 --- a/nutype_macros/src/common/gen/mod.rs +++ b/nutype_macros/src/common/gen/mod.rs @@ -9,7 +9,8 @@ use std::{collections::HashSet, hash::Hash}; use self::traits::GeneratedTraits; use super::models::{ - ErrorTypeName, GenerateParams, Guard, NewUnchecked, ParseErrorTypeName, TypeName, TypeTrait, + CustomFunction, ErrorTypeName, GenerateParams, Guard, NewUnchecked, ParseErrorTypeName, + TypeName, TypeTrait, }; use crate::common::{ gen::{new_unchecked::gen_new_unchecked, parse_error::gen_parse_error_name}, @@ -247,14 +248,33 @@ pub trait GenerateNewtype { generics: &Generics, inner_type: &Self::InnerType, sanitizers: &[Self::Sanitizer], - validators: &[Self::Validator], - error_type_name: &ErrorTypeName, + validation: &Validation, ) -> TokenStream { let generics_without_bounds = strip_trait_bounds_on_generics(generics); let fn_sanitize = Self::gen_fn_sanitize(inner_type, sanitizers); - let validation_error = - Self::gen_validation_error_type(type_name, error_type_name, validators); - let fn_validate = Self::gen_fn_validate(inner_type, error_type_name, validators); + + let maybe_generated_validation_error = match validation { + Validation::Standard { + validators, + error_type_name, + } => { + let validation_error = + Self::gen_validation_error_type(type_name, error_type_name, validators); + Some(validation_error) + } + Validation::Custom { .. } => None, + }; + + let fn_validate = match validation { + Validation::Standard { + validators, + error_type_name, + } => Self::gen_fn_validate(inner_type, error_type_name, validators), + Validation::Custom { + with, + error_type_name, + } => gen_fn_validate_custom(inner_type, with, error_type_name), + }; let (input_type, convert_raw_value_if_necessary) = if Self::NEW_CONVERT_INTO_INNER_TYPE { ( @@ -265,8 +285,10 @@ pub trait GenerateNewtype { (quote!(#inner_type), quote!()) }; + let error_type_name = validation.error_type_name(); + quote!( - #validation_error + #maybe_generated_validation_error impl #generics #type_name #generics_without_bounds { pub fn try_new(raw_value: #input_type) -> ::core::result::Result { @@ -336,19 +358,7 @@ pub trait GenerateNewtype { Guard::WithValidation { sanitizers, validation, - } => match validation { - Validation::Standard { - validators, - error_type_name, - } => Self::gen_try_new( - type_name, - generics, - inner_type, - sanitizers, - validators, - error_type_name, - ), - }, + } => Self::gen_try_new(type_name, generics, inner_type, sanitizers, validation), }; let impl_into_inner = gen_impl_into_inner(type_name, generics, inner_type); let impl_new_unchecked = gen_new_unchecked(type_name, inner_type, new_unchecked); @@ -400,13 +410,22 @@ pub trait GenerateNewtype { &traits, ); - let maybe_error_type_name = guard.maybe_error_type_name(); + let maybe_reimported_error_type_name = match &guard { + Guard::WithoutValidation { .. } => None, + Guard::WithValidation { validation, .. } => match validation { + // We won't need to reimport error if it's a custom error provided by the user. + Validation::Custom { .. } => None, + Validation::Standard { + error_type_name, .. + } => Some(error_type_name), + }, + }; let reimports = gen_reimports( vis, &type_name, &module_name, - maybe_error_type_name, + maybe_reimported_error_type_name, maybe_parse_error_type_name.as_ref(), ); @@ -449,3 +468,18 @@ pub trait GenerateNewtype { traits: &HashSet, ) -> TokenStream; } + +fn gen_fn_validate_custom( + inner_type: &InnerType, + with: &CustomFunction, + error_type_name: &ErrorTypeName, +) -> TokenStream { + quote! { + // For some types like `String` clippy suggests using `&str` instead of `&String` here, + // but it does not really matter in this context. + #[allow(clippy::ptr_arg)] + fn __validate__(value: &#inner_type) -> ::core::result::Result<(), #error_type_name> { + #with(value) + } + } +} diff --git a/nutype_macros/src/common/models.rs b/nutype_macros/src/common/models.rs index d965e2f..de57cd9 100644 --- a/nutype_macros/src/common/models.rs +++ b/nutype_macros/src/common/models.rs @@ -16,6 +16,7 @@ use crate::{ }; use super::gen::type_custom_closure; +use super::parse::RawValidation; /// A spanned item. An item can be anything that cares a domain value. /// Keeping a span allows to throw good precise error messages at the validation stage. @@ -152,6 +153,13 @@ impl ErrorTypeName { } } +impl Parse for ErrorTypeName { + fn parse(input: ParseStream) -> syn::Result { + let ident = input.parse::()?; + Ok(Self::new(ident)) + } +} + // A type that represents an error name which is returned by `FromStr` traits. // For example, if `TypeName` is `Amount`, then this would be `AmountParseError`. define_ident_type!(ParseErrorTypeName); @@ -216,26 +224,41 @@ pub enum Guard { #[derive(Debug)] pub enum Validation { - // TODO: - // Custom { - // with: TypedCustomFunction, - // error_type_name: ErrorTypeName, - // }, - Standard { + Custom { + /// Custom validation function that should return `Result<(), ErrorType>` + with: CustomFunction, + + /// Name of the error type. Since the type is defined by user, the macro must not generate + /// it. error_type_name: ErrorTypeName, + }, + Standard { + /// List of the standard validators validators: Vec, + + /// Name of the error type. The #[nutype] macro must generate definition of this type. + error_type_name: ErrorTypeName, }, } +impl Validation { + pub fn error_type_name(&self) -> &ErrorTypeName { + match self { + Self::Custom { + error_type_name, .. + } => error_type_name, + Self::Standard { + error_type_name, .. + } => error_type_name, + } + } +} + impl Guard { pub fn maybe_error_type_name(&self) -> Option<&ErrorTypeName> { match self { Self::WithoutValidation { .. } => None, - Self::WithValidation { validation, .. } => match validation { - Validation::Standard { - error_type_name, .. - } => Some(error_type_name), - }, + Self::WithValidation { validation, .. } => Some(validation.error_type_name()), } } } @@ -303,10 +326,11 @@ impl Guard { } } - pub fn validators(&self) -> Option<&Vec> { + pub fn standard_validators(&self) -> Option<&Vec> { match self { Self::WithValidation { validation, .. } => match validation { Validation::Standard { validators, .. } => Some(validators), + Validation::Custom { .. } => None, }, Self::WithoutValidation { .. } => None, } @@ -317,8 +341,7 @@ impl Guard { #[derive(Debug)] pub struct RawGuard { pub sanitizers: Vec, - pub validators: Vec, - pub error_type_name: Option, + pub validation: Option>, } #[derive(Debug, Clone, Copy, PartialEq, Eq)] diff --git a/nutype_macros/src/common/parse/mod.rs b/nutype_macros/src/common/parse/mod.rs index bff2701..b6973d0 100644 --- a/nutype_macros/src/common/parse/mod.rs +++ b/nutype_macros/src/common/parse/mod.rs @@ -1,9 +1,14 @@ pub mod derive_trait; pub mod meta; -use std::{any::type_name, fmt::Debug, str::FromStr}; +use std::{ + any::type_name, + fmt::{Debug, Display}, + str::FromStr, +}; use cfg_if::cfg_if; +use kinded::{Kind, Kinded}; use proc_macro2::{Ident, Span}; use syn::{ parenthesized, @@ -56,7 +61,7 @@ pub struct ParseableAttributes { pub sanitizers: Vec, /// Parsed from `validate(...)` attribute - pub validators: Vec, + pub validation: Option>, /// Parsed from `new_unchecked` attribute pub new_unchecked: NewUnchecked, @@ -66,9 +71,140 @@ pub struct ParseableAttributes { /// Parsed from `derive(...)` attribute pub derive_traits: Vec, +} + +enum ValidateAttr { + Standard(Validator), + Extra(ExtraValidateAttr), +} + +/// Non standard (custom) validation attributes. +/// Responsible for parsing `error` and `with` attributes. +#[derive(Debug, Kinded)] +#[kinded(display = "snake_case")] +enum ExtraValidateAttr { + Error(ErrorTypeName), + With(CustomFunction), +} + +impl Parse for ExtraValidateAttr { + fn parse(input: ParseStream) -> syn::Result { + let (kind, _ident) = parse_validator_kind(input)?; + match kind { + ExtraValidateAttrKind::Error => { + let _eq: Token![=] = input.parse()?; + let error: ErrorTypeName = input.parse()?; + Ok(ExtraValidateAttr::Error(error)) + } + ExtraValidateAttrKind::With => { + let _eq: Token![=] = input.parse()?; + let custom_function: CustomFunction = input.parse()?; + Ok(ExtraValidateAttr::With(custom_function)) + } + } + } +} - /// TODO: not implemented yet - pub error_type_name: Option, +impl Parse for ValidateAttr +where + Validator: Parse + Kinded, + ::Kind: Kind + Display + 'static, +{ + /// Try to parse either standard validation attributes or combination of `error` and `with` attributes. + fn parse(input: ParseStream) -> syn::Result { + // NOTE: ParseStream has interior mutability, so we want to try to parse validator, + // but we don't want to advance the input if it fails. + if input.fork().parse::().is_ok() { + let validator: Validator = input.parse()?; + Ok(ValidateAttr::Standard(validator)) + } else if input.fork().parse::().is_ok() { + let extra_attr: ExtraValidateAttr = input.parse()?; + Ok(ValidateAttr::Extra(extra_attr)) + } else { + let possible_values: String = ::Kind::all() + .iter() + .map(|k| format!("`{k}`")) + .filter(|s| s != "`phantom`") // filter out _Phantom variant + .chain(["`with`", "`error`"].iter().map(|s| s.to_string())) + .collect::>() + .join(", "); + let ident: Ident = input.parse()?; + let msg = format!("Unknown validation attribute: `{ident}`.\nPossible attributes are {possible_values}."); + Err(syn::Error::new(ident.span(), msg)) + } + } +} + +#[derive(Debug)] +pub enum RawValidation { + Custom { + with: CustomFunction, + error: ErrorTypeName, + }, + Standard { + validators: Vec, + }, +} + +impl Parse for RawValidation +where + Validator: Parse + Kinded, + ::Kind: Kind + Display + 'static, +{ + fn parse(input: ParseStream) -> syn::Result { + let items = input.parse_terminated(ValidateAttr::::parse, Token![,])?; + let attrs: Vec> = items.into_iter().collect(); + + let mut validators: Vec = Vec::new(); + let mut maybe_with: Option = None; + let mut maybe_error: Option = None; + + for attr in attrs { + match attr { + ValidateAttr::Standard(validator) => { + validators.push(validator); + } + ValidateAttr::Extra(extra_attr) => match extra_attr { + ExtraValidateAttr::Error(error) => { + if maybe_error.is_some() { + let msg = "Duplicate `error` attribute"; + return Err(syn::Error::new(error.span(), msg)); + } + maybe_error = Some(error); + } + ExtraValidateAttr::With(with) => { + if maybe_with.is_some() { + let msg = "Duplicate `with` attribute"; + return Err(syn::Error::new(with.span(), msg)); + } + maybe_with = Some(with); + } + }, + } + } + + match (validators.len(), maybe_with, maybe_error) { + (0, Some(with), Some(error)) => Ok(RawValidation::Custom { with, error }), + (0, Some(_), None) => { + let msg = "The `with` attribute requires an accompanying `error` attribute.\nPlease provide the error type that the `with` validation function returns."; + Err(syn::Error::new(input.span(), msg)) + } + (0, None, Some(error_type)) => { + let msg = format!("The `error` attribute requires an accompanying `with` attribute.\nPlease provide the validation function that returns Result<(), {error_type}>."); + Err(syn::Error::new(input.span(), msg)) + } + (0, None, None) => { + let msg = "At least one validator must be specified"; + Err(syn::Error::new(input.span(), msg)) + } + (_, None, None) => Ok(RawValidation::Standard { validators }), + (_, _, _) => { + let msg = + "`with` and `error` attributes cannot be used mixed with other validators."; + Err(syn::Error::new(input.span(), msg)) + } + } + } } // By some reason Default cannot be derived. @@ -76,16 +212,20 @@ impl Default for ParseableAttributes fn default() -> Self { Self { sanitizers: vec![], - validators: vec![], + validation: None, new_unchecked: NewUnchecked::Off, default: None, derive_traits: vec![], - error_type_name: None, } } } -impl Parse for ParseableAttributes { +impl Parse for ParseableAttributes +where + Sanitizer: Parse, + Validator: Parse + Kinded, + ::Kind: Kind + Display + 'static, +{ fn parse(input: ParseStream) -> syn::Result { let mut attrs = ParseableAttributes::default(); @@ -109,8 +249,8 @@ impl Parse for ParseableAttributes = content.parse()?; + attrs.validation = Some(validation); } else { let msg = concat!( "`validate` must be used with parenthesis.\n", @@ -244,7 +384,7 @@ where parse_kind("validator", input) } -/// Parse ident from ParStream and tries to parse it further into Kind of sanitizier or validator. +/// Parse ident from ParseStream and tries to parse it further into Kind of sanitizer or validator. /// Build a helpful error on failure. fn parse_kind(attr_type: &str, input: ParseStream) -> syn::Result<(K, Ident)> where diff --git a/nutype_macros/src/common/validate.rs b/nutype_macros/src/common/validate.rs index 1597025..696efca 100644 --- a/nutype_macros/src/common/validate.rs +++ b/nutype_macros/src/common/validate.rs @@ -6,6 +6,7 @@ use super::{ DeriveTrait, Guard, NumericBoundValidator, RawGuard, SpannedDeriveTrait, SpannedItem, TypeName, Validation, }, + parse::RawValidation, r#gen::error::gen_error_type_name, }; @@ -18,29 +19,36 @@ pub fn validate_guard( ) -> Result, syn::Error> { let RawGuard { sanitizers: raw_sanitizers, - validators: raw_validators, - error_type_name, + validation: maybe_raw_validation, } = raw_guard; - let validators = validate_validators(raw_validators)?; let sanitizers = validate_sanitizers(raw_sanitizers)?; - if validators.is_empty() { - if let Some(error_type_name) = error_type_name { - let msg = "error_type_name cannot be set when there is no validation"; - return Err(syn::Error::new(error_type_name.span(), msg)); - } - Ok(Guard::WithoutValidation { sanitizers }) - } else { - let error_type_name = error_type_name.unwrap_or_else(|| gen_error_type_name(type_name)); - Ok(Guard::WithValidation { - sanitizers, - validation: Validation::Standard { + let Some(raw_validation) = maybe_raw_validation else { + return Ok(Guard::WithoutValidation { sanitizers }); + }; + + let validation = match raw_validation { + RawValidation::Standard { validators } => { + let error_type_name = gen_error_type_name(type_name); + let validators = validate_validators(validators)?; + Validation::Standard { validators, error_type_name, - }, - }) - } + } + } + RawValidation::Custom { with, error } => { + let error_type_name = error; + Validation::Custom { + with, + error_type_name, + } + } + }; + Ok(Guard::WithValidation { + sanitizers, + validation, + }) } pub fn validate_duplicates( diff --git a/nutype_macros/src/float/gen/mod.rs b/nutype_macros/src/float/gen/mod.rs index 3749c7b..993ef91 100644 --- a/nutype_macros/src/float/gen/mod.rs +++ b/nutype_macros/src/float/gen/mod.rs @@ -159,7 +159,7 @@ where guard: &Guard, _traits: &HashSet, ) -> TokenStream { - let test_lower_vs_upper = guard.validators().and_then(|validators| { + let test_lower_vs_upper = guard.standard_validators().and_then(|validators| { gen_test_should_have_consistent_lower_and_upper_boundaries(type_name, validators) }); diff --git a/nutype_macros/src/float/gen/traits/arbitrary.rs b/nutype_macros/src/float/gen/traits/arbitrary.rs index 254102e..1e90a63 100644 --- a/nutype_macros/src/float/gen/traits/arbitrary.rs +++ b/nutype_macros/src/float/gen/traits/arbitrary.rs @@ -74,6 +74,11 @@ fn gen_generate_valid_inner_value( inner_type, sanitizers, validators, ) } + Validation::Custom { .. } => { + let span = Span::call_site(); + let msg = "It's not possible to derive `Arbitrary` trait for a type with custom validation.\nYou have to implement `Arbitrary` trait on you own."; + Err(syn::Error::new(span, msg)) + } } } } diff --git a/nutype_macros/src/float/parse.rs b/nutype_macros/src/float/parse.rs index f0ef464..26ec7a9 100644 --- a/nutype_macros/src/float/parse.rs +++ b/nutype_macros/src/float/parse.rs @@ -37,16 +37,14 @@ where let ParseableAttributes { sanitizers, - validators, + validation, new_unchecked, default, derive_traits, - error_type_name, } = attrs; let raw_guard = FloatRawGuard { sanitizers, - validators, - error_type_name, + validation, }; let guard = validate_float_guard(raw_guard, type_name)?; Ok(Attributes { diff --git a/nutype_macros/src/float/validate.rs b/nutype_macros/src/float/validate.rs index 93b32cd..85bc148 100644 --- a/nutype_macros/src/float/validate.rs +++ b/nutype_macros/src/float/validate.rs @@ -65,6 +65,7 @@ fn has_validation_against_nan(guard: &FloatGuard) -> bool { match guard { FloatGuard::WithoutValidation { .. } => false, FloatGuard::WithValidation { validation, .. } => match validation { + Validation::Custom { .. } => false, Validation::Standard { validators, .. } => validators .iter() .any(|v| v.kind() == FloatValidatorKind::Finite), diff --git a/nutype_macros/src/integer/gen/mod.rs b/nutype_macros/src/integer/gen/mod.rs index 6fbe508..6dbd866 100644 --- a/nutype_macros/src/integer/gen/mod.rs +++ b/nutype_macros/src/integer/gen/mod.rs @@ -151,7 +151,7 @@ where guard: &Guard, _traits: &HashSet, ) -> TokenStream { - let test_lower_vs_upper = guard.validators().and_then(|validators| { + let test_lower_vs_upper = guard.standard_validators().and_then(|validators| { gen_test_should_have_consistent_lower_and_upper_boundaries(type_name, validators) }); diff --git a/nutype_macros/src/integer/gen/traits/arbitrary.rs b/nutype_macros/src/integer/gen/traits/arbitrary.rs index 0701d23..b9e0640 100644 --- a/nutype_macros/src/integer/gen/traits/arbitrary.rs +++ b/nutype_macros/src/integer/gen/traits/arbitrary.rs @@ -69,6 +69,12 @@ fn guard_to_boundary( validation, } => { match validation { + Validation::Custom { .. } => { + return Err(syn::Error::new( + proc_macro2::Span::call_site(), + "Cannot derive trait `Arbitrary` for a type with custom `with` validator.", + )); + } Validation::Standard { validators, error_type_name: _, diff --git a/nutype_macros/src/integer/parse.rs b/nutype_macros/src/integer/parse.rs index 74a80fd..690ec47 100644 --- a/nutype_macros/src/integer/parse.rs +++ b/nutype_macros/src/integer/parse.rs @@ -37,16 +37,14 @@ where let ParseableAttributes { sanitizers, - validators, + validation, new_unchecked, default, derive_traits, - error_type_name, } = attrs; let raw_guard = IntegerRawGuard { sanitizers, - validators, - error_type_name, + validation, }; let guard = validate_integer_guard(raw_guard, type_name)?; Ok(Attributes { diff --git a/nutype_macros/src/string/gen/mod.rs b/nutype_macros/src/string/gen/mod.rs index b0a6ef0..8b2ecef 100644 --- a/nutype_macros/src/string/gen/mod.rs +++ b/nutype_macros/src/string/gen/mod.rs @@ -190,7 +190,7 @@ impl GenerateNewtype for StringNewtype { guard: &Guard, _traits: &HashSet, ) -> TokenStream { - let test_len_char_min_vs_max = guard.validators().and_then(|validators| { + let test_len_char_min_vs_max = guard.standard_validators().and_then(|validators| { tests::gen_test_should_have_consistent_len_char_boundaries(type_name, validators) }); diff --git a/nutype_macros/src/string/gen/traits/arbitrary.rs b/nutype_macros/src/string/gen/traits/arbitrary.rs index 6811783..348befc 100644 --- a/nutype_macros/src/string/gen/traits/arbitrary.rs +++ b/nutype_macros/src/string/gen/traits/arbitrary.rs @@ -112,10 +112,9 @@ fn build_specification(guard: &StringGuard) -> Result, syn sanitizers, validation, } => { - let validators = get_validators(validation); - - let relevant_sanitizers = filter_sanitizers(sanitizers)?; + let validators = get_validators(validation)?; let relevant_validators = filter_validators(validators)?; + let relevant_sanitizers = filter_sanitizers(sanitizers)?; let has_trim = relevant_sanitizers .iter() @@ -151,9 +150,15 @@ fn build_specification(guard: &StringGuard) -> Result, syn } } -fn get_validators(validation: &Validation) -> &[StringValidator] { +fn get_validators( + validation: &Validation, +) -> Result<&[StringValidator], syn::Error> { match validation { - Validation::Standard { validators, .. } => validators, + Validation::Standard { validators, .. } => Ok(validators), + Validation::Custom { .. } => { + let msg = "It's not possible to derive `Arbitrary` trait for a type with custom validation.\nYou have to implement `Arbitrary` trait on you own."; + Err(syn::Error::new(Span::call_site(), msg)) + } } } diff --git a/nutype_macros/src/string/parse.rs b/nutype_macros/src/string/parse.rs index e935c68..ed73d5c 100644 --- a/nutype_macros/src/string/parse.rs +++ b/nutype_macros/src/string/parse.rs @@ -33,16 +33,14 @@ pub fn parse_attributes( let ParseableAttributes { sanitizers, - validators, + validation, new_unchecked, default, derive_traits, - error_type_name, } = attrs; let raw_guard = StringRawGuard { sanitizers, - validators, - error_type_name, + validation, }; let guard = validate_string_guard(raw_guard, type_name)?; Ok(Attributes { diff --git a/test_suite/Cargo.toml b/test_suite/Cargo.toml index 40d94c7..b6b5662 100644 --- a/test_suite/Cargo.toml +++ b/test_suite/Cargo.toml @@ -20,6 +20,7 @@ arbtest = "0.2.0" ron = "0.8.1" rmp-serde = "1.1.2" num = "0.4.3" +thiserror = "1.0.63" [features] serde = ["nutype/serde", "dep:serde", "dep:serde_json"] diff --git a/test_suite/tests/any.rs b/test_suite/tests/any.rs index 38f790a..d40a718 100644 --- a/test_suite/tests/any.rs +++ b/test_suite/tests/any.rs @@ -924,3 +924,49 @@ mod with_generics { assert_eq!(collection.into_inner(), vec![0]); } } + +mod custom_error { + use super::*; + use thiserror::Error; + + #[nutype( + validate(with = validate_decent_collection, error = DecentCollectionError), + derive(Debug, PartialEq, AsRef), + )] + struct DecentCollection(Vec); + + fn validate_decent_collection(collection: &[T]) -> Result<(), DecentCollectionError> { + if collection.len() < 3 { + Err(DecentCollectionError::TooShort) + } else if collection.len() > 10 { + Err(DecentCollectionError::TooLong) + } else { + Ok(()) + } + } + + #[derive(Error, Debug, PartialEq)] + enum DecentCollectionError { + #[error("Collection is too short.")] + TooShort, + + #[error("Collection is too long.")] + TooLong, + } + + #[test] + fn test_custom_error() { + assert_eq!( + DecentCollection::try_new(vec![1, 2]), + Err(DecentCollectionError::TooShort) + ); + + assert_eq!( + DecentCollection::try_new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + Err(DecentCollectionError::TooLong) + ); + + let collection = DecentCollection::try_new(vec![1, 2, 3]).unwrap(); + assert_eq!(collection.as_ref(), &[1, 2, 3]); + } +} diff --git a/test_suite/tests/float.rs b/test_suite/tests/float.rs index 78aa15e..b972461 100644 --- a/test_suite/tests/float.rs +++ b/test_suite/tests/float.rs @@ -757,3 +757,49 @@ mod derive_schemars_json_schema { let _schema = schema_for!(ProductWeight); } } + +mod custom_error { + use super::*; + use thiserror::Error; + + #[nutype( + validate(with = validate_oh_my_float, error = OhMyFloatError), + derive(Debug, FromStr), + )] + struct OhMyFloat(f64); + + #[derive(Error, Debug, PartialEq)] + enum OhMyFloatError { + #[error("Oh no! The value is too big!.")] + TooBig, + + #[error("Oh no! The value is too small!.")] + TooSmall, + } + + fn validate_oh_my_float(value: &f64) -> Result<(), OhMyFloatError> { + if *value > 100.0 { + Err(OhMyFloatError::TooBig) + } else if *value < 0.0 { + Err(OhMyFloatError::TooSmall) + } else { + Ok(()) + } + } + + #[test] + fn test_custom_error() { + { + let err = OhMyFloat::try_new(101.0).unwrap_err(); + assert_eq!(err, OhMyFloatError::TooBig); + } + + { + let err = OhMyFloat::try_new(-1.0).unwrap_err(); + assert_eq!(err, OhMyFloatError::TooSmall); + } + + let oh_my_float = OhMyFloat::try_new(99.0).unwrap(); + assert_eq!(oh_my_float.into_inner(), 99.0); + } +} diff --git a/test_suite/tests/integer.rs b/test_suite/tests/integer.rs index c50855f..eea1c59 100644 --- a/test_suite/tests/integer.rs +++ b/test_suite/tests/integer.rs @@ -832,3 +832,51 @@ mod derive_schemars_json_schema { let _schema = schema_for!(CustomerId); } } + +mod custom_error { + use super::*; + 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(()) + } + + #[test] + fn test_custom_error() { + { + let err = PositivelyOdd::try_new(-1).unwrap_err(); + assert_eq!(err, PositivelyOddError::Negative); + } + + { + 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); + } +} diff --git a/test_suite/tests/string.rs b/test_suite/tests/string.rs index 22683b7..d201407 100644 --- a/test_suite/tests/string.rs +++ b/test_suite/tests/string.rs @@ -675,3 +675,46 @@ mod validation_with_regex { assert_eq!(inner, "123-456".to_string()); } } + +mod custom_error { + use super::*; + use thiserror::Error; + + #[nutype( + validate(with = validate_name, error = NameError), + derive(Debug, AsRef, PartialEq), + )] + struct Name(String); + + fn validate_name(name: &str) -> Result<(), NameError> { + if name.len() < 3 { + Err(NameError::TooShort) + } else if name.len() > 10 { + Err(NameError::TooLong) + } else { + Ok(()) + } + } + + #[derive(Error, Debug, PartialEq)] + enum NameError { + #[error("Name is too short.")] + TooShort, + + #[error("Name is too long.")] + TooLong, + } + + #[test] + fn test_custom_error() { + assert_eq!( + Name::try_new("JohnJohnJohnJohnJohn"), + Err(NameError::TooLong) + ); + + assert_eq!(Name::try_new("Jo"), Err(NameError::TooShort)); + + let name = Name::try_new("John").unwrap(); + assert_eq!(name.as_ref(), "John"); + } +} 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 31b1f50..25fb4a1 100644 --- a/test_suite/tests/ui/common/attribute_with_wrong_case.stderr +++ b/test_suite/tests/ui/common/attribute_with_wrong_case.stderr @@ -1,4 +1,5 @@ -error: Unknown validator `lenCharMax`. Did you mean `len_char_max`? +error: Unknown validation attribute: `lenCharMax`. + Possible attributes are `len_char_min`, `len_char_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/common/custom_validaiton_no_with.rs b/test_suite/tests/ui/common/custom_validaiton_no_with.rs new file mode 100644 index 0000000..0cc12f1 --- /dev/null +++ b/test_suite/tests/ui/common/custom_validaiton_no_with.rs @@ -0,0 +1,14 @@ +use nutype::nutype; + +#[nutype( + validate(error = NumError) +)] +pub struct Num(i32); + +#[derive(Debug)] +enum NumError { + TooBig, + TooSmall, +} + +fn main () {} diff --git a/test_suite/tests/ui/common/custom_validaiton_no_with.stderr b/test_suite/tests/ui/common/custom_validaiton_no_with.stderr new file mode 100644 index 0000000..b4ddb6b --- /dev/null +++ b/test_suite/tests/ui/common/custom_validaiton_no_with.stderr @@ -0,0 +1,6 @@ +error: The `error` attribute requires an accompanying `with` attribute. + Please provide the validation function that returns Result<(), NumError>. + --> tests/ui/common/custom_validaiton_no_with.rs:4:30 + | +4 | validate(error = NumError) + | ^ diff --git a/test_suite/tests/ui/common/custom_validation_mixed.rs b/test_suite/tests/ui/common/custom_validation_mixed.rs new file mode 100644 index 0000000..38f8309 --- /dev/null +++ b/test_suite/tests/ui/common/custom_validation_mixed.rs @@ -0,0 +1,24 @@ +use nutype::nutype; + +#[nutype( + validate(with = validate_num, error = NumError, predicate = |val| *val > 0 ) +)] +pub struct Num(i32); + +fn validate_num(val: &i32) -> Result<(), NumError> { + if *val > 100 { + Err(NumError::TooBig) + } else if *val < 0 { + Err(NumError::TooSmall) + } else { + Ok(()) + } +} + +#[derive(Debug)] +enum NumError { + TooBig, + TooSmall, +} + +fn main () {} diff --git a/test_suite/tests/ui/common/custom_validation_mixed.stderr b/test_suite/tests/ui/common/custom_validation_mixed.stderr new file mode 100644 index 0000000..52f81d3 --- /dev/null +++ b/test_suite/tests/ui/common/custom_validation_mixed.stderr @@ -0,0 +1,5 @@ +error: `with` and `error` attributes cannot be used mixed with other validators. + --> tests/ui/common/custom_validation_mixed.rs:4:80 + | +4 | validate(with = validate_num, error = NumError, predicate = |val| *val > 0 ) + | ^ diff --git a/test_suite/tests/ui/common/custom_validation_no_error.stderr b/test_suite/tests/ui/common/custom_validation_no_error.stderr new file mode 100644 index 0000000..b5d2f21 --- /dev/null +++ b/test_suite/tests/ui/common/custom_validation_no_error.stderr @@ -0,0 +1,6 @@ +error: The `with` attribute requires an accompanying `error` attribute. + Please provide the error type that the `with` validation function returns. + --> tests/ui/common/custom_validation_no_error.rs:4:34 + | +4 | validate(with = validate_num,) + | ^ diff --git a/test_suite/tests/ui/float/validate/unknown.stderr b/test_suite/tests/ui/float/validate/unknown.stderr index 0e618c3..cbeafd7 100644 --- a/test_suite/tests/ui/float/validate/unknown.stderr +++ b/test_suite/tests/ui/float/validate/unknown.stderr @@ -1,5 +1,5 @@ -error: Unknown validator `meaningful`. - Possible values are `greater`, `greater_or_equal`, `less`, `less_or_equal`, `predicate`, `finite`. +error: Unknown validation attribute: `meaningful`. + Possible attributes are `greater`, `greater_or_equal`, `less`, `less_or_equal`, `predicate`, `finite`, `with`, `error`. --> tests/ui/float/validate/unknown.rs:3:19 | 3 | #[nutype(validate(meaningful))] diff --git a/test_suite/tests/ui/integer/validate/unknown.stderr b/test_suite/tests/ui/integer/validate/unknown.stderr index a946f1b..b418f05 100644 --- a/test_suite/tests/ui/integer/validate/unknown.stderr +++ b/test_suite/tests/ui/integer/validate/unknown.stderr @@ -1,5 +1,5 @@ -error: Unknown validator `meaningful`. - Possible values are `greater`, `greater_or_equal`, `less`, `less_or_equal`, `predicate`. +error: Unknown validation attribute: `meaningful`. + Possible attributes are `greater`, `greater_or_equal`, `less`, `less_or_equal`, `predicate`, `with`, `error`. --> tests/ui/integer/validate/unknown.rs:3:19 | 3 | #[nutype(validate(meaningful))] diff --git a/test_suite/tests/ui/string/validate/unknown.stderr b/test_suite/tests/ui/string/validate/unknown.stderr index 80eab51..10941f6 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 validator `unique`. - Possible values are `len_char_min`, `len_char_max`, `not_empty`, `predicate`, `regex`. +error: Unknown validation attribute: `unique`. + Possible attributes are `len_char_min`, `len_char_max`, `not_empty`, `predicate`, `regex`, `with`, `error`. --> tests/ui/string/validate/unknown.rs:3:19 | 3 | #[nutype(validate(unique))]