From d591184884b77554811fa8b56c37d47a97cdf0fb Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 10 Aug 2024 17:43:48 +0200 Subject: [PATCH 01/16] Add Validation::Custom variant --- nutype_macros/src/common/gen/mod.rs | 61 +++++++++++++------ nutype_macros/src/common/models.rs | 33 ++++++---- nutype_macros/src/float/gen/mod.rs | 2 +- .../src/float/gen/traits/arbitrary.rs | 5 ++ nutype_macros/src/float/validate.rs | 1 + nutype_macros/src/integer/gen/mod.rs | 2 +- .../src/integer/gen/traits/arbitrary.rs | 6 ++ nutype_macros/src/string/gen/mod.rs | 2 +- .../src/string/gen/traits/arbitrary.rs | 15 +++-- 9 files changed, 87 insertions(+), 40 deletions(-) diff --git a/nutype_macros/src/common/gen/mod.rs b/nutype_macros/src/common/gen/mod.rs index 5dae0c3..7d36460 100644 --- a/nutype_macros/src/common/gen/mod.rs +++ b/nutype_macros/src/common/gen/mod.rs @@ -13,7 +13,7 @@ use super::models::{ }; use crate::common::{ gen::{new_unchecked::gen_new_unchecked, parse_error::gen_parse_error_name}, - models::{ModuleName, Validation}, + models::{ModuleName, TypedCustomFunction, Validation}, }; use proc_macro2::{Punct, Spacing, TokenStream, TokenTree}; use quote::{format_ident, quote, ToTokens}; @@ -247,14 +247,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 +284,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 +357,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); @@ -449,3 +458,15 @@ pub trait GenerateNewtype { traits: &HashSet, ) -> TokenStream; } + +fn gen_fn_validate_custom( + inner_type: &InnerType, + with: &TypedCustomFunction, + error_type_name: &ErrorTypeName, +) -> TokenStream { + quote! { + 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..e4b43b0 100644 --- a/nutype_macros/src/common/models.rs +++ b/nutype_macros/src/common/models.rs @@ -216,26 +216,34 @@ pub enum Guard { #[derive(Debug)] pub enum Validation { - // TODO: - // Custom { - // with: TypedCustomFunction, - // error_type_name: ErrorTypeName, - // }, - Standard { + Custom { + with: TypedCustomFunction, error_type_name: ErrorTypeName, + }, + Standard { validators: Vec, + 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 +311,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, } 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/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/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)) + } } } From 191a7b0e8975acae9fd08a46a2d4624473a1a489 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Fri, 16 Aug 2024 21:15:31 +0200 Subject: [PATCH 02/16] Fix ui tests --- dummy/src/main.rs | 24 +-- nutype_macros/src/any/parse.rs | 6 +- nutype_macros/src/common/gen/mod.rs | 21 ++- nutype_macros/src/common/models.rs | 13 +- nutype_macros/src/common/parse/mod.rs | 157 ++++++++++++++++-- nutype_macros/src/common/validate.rs | 39 +++-- nutype_macros/src/float/parse.rs | 6 +- nutype_macros/src/integer/parse.rs | 6 +- nutype_macros/src/string/parse.rs | 6 +- .../common/attribute_with_wrong_case.stderr | 3 +- .../tests/ui/float/validate/unknown.stderr | 4 +- .../tests/ui/integer/validate/unknown.stderr | 4 +- .../tests/ui/string/validate/unknown.stderr | 4 +- 13 files changed, 228 insertions(+), 65 deletions(-) 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/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 7d36460..3b790bd 100644 --- a/nutype_macros/src/common/gen/mod.rs +++ b/nutype_macros/src/common/gen/mod.rs @@ -9,11 +9,12 @@ 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}, - models::{ModuleName, TypedCustomFunction, Validation}, + models::{ModuleName, Validation}, }; use proc_macro2::{Punct, Spacing, TokenStream, TokenTree}; use quote::{format_ident, quote, ToTokens}; @@ -409,7 +410,16 @@ pub trait GenerateNewtype { &traits, ); - let maybe_error_type_name = guard.maybe_error_type_name(); + let maybe_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, @@ -461,10 +471,13 @@ pub trait GenerateNewtype { fn gen_fn_validate_custom( inner_type: &InnerType, - with: &TypedCustomFunction, + 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 e4b43b0..9753967 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); @@ -217,7 +225,7 @@ pub enum Guard { #[derive(Debug)] pub enum Validation { Custom { - with: TypedCustomFunction, + with: CustomFunction, error_type_name: ErrorTypeName, }, Standard { @@ -326,8 +334,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..0ca23c2 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,137 @@ pub struct ParseableAttributes { /// Parsed from `derive(...)` attribute pub derive_traits: Vec, +} + +enum ValidateAttr { + Standard(Validator), + Extra(ExtraValidateAttr), +} + +#[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, +{ + 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 = "`with` attribute must be used with `error` attribute"; + Err(syn::Error::new(input.span(), msg)) + } + (0, None, Some(_)) => { + let msg = "`error` attribute must be used with `with` attribute"; + 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 +209,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 +246,8 @@ impl Parse for ParseableAttributes = content.parse()?; + attrs.validation = Some(validation); } else { let msg = concat!( "`validate` must be used with parenthesis.\n", @@ -244,7 +381,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..f6792f8 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,28 +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 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)); + if let Some(raw_validation) = maybe_raw_validation { + 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: Validation::Standard { - validators, - error_type_name, - }, + validation, }) + } else { + Ok(Guard::WithoutValidation { sanitizers }) } } 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/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/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/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/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))] From c78b4266a0b2916577921e819b23adc9abc436a1 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Thu, 22 Aug 2024 21:54:58 +0200 Subject: [PATCH 03/16] Implement integration tests for custom errors --- Cargo.lock | 29 ++++++++++++++++ Cargo.toml | 2 +- examples/custom_error/Cargo.toml | 8 +++++ examples/custom_error/src/main.rs | 43 ++++++++++++++++++++++++ test_suite/Cargo.toml | 1 + test_suite/tests/any.rs | 43 ++++++++++++++++++++++++ test_suite/tests/float.rs | 46 +++++++++++++++++++++++++ test_suite/tests/integer.rs | 56 +++++++++++++++++++++++++++++++ test_suite/tests/string.rs | 43 ++++++++++++++++++++++++ 9 files changed, 270 insertions(+), 1 deletion(-) create mode 100644 examples/custom_error/Cargo.toml create mode 100644 examples/custom_error/src/main.rs 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..643bd55 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,5 +19,5 @@ 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/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..da07ee8 --- /dev/null +++ b/examples/custom_error/src/main.rs @@ -0,0 +1,43 @@ +use nutype::nutype; +use thiserror::Error; + +#[nutype( + validate(with = validate_positive_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_positive_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/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..43366ae 100644 --- a/test_suite/tests/any.rs +++ b/test_suite/tests/any.rs @@ -924,3 +924,46 @@ 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), + )] + 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!( + 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/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..4fb60d4 100644 --- a/test_suite/tests/integer.rs +++ b/test_suite/tests/integer.rs @@ -832,3 +832,59 @@ mod derive_schemars_json_schema { let _schema = schema_for!(CustomerId); } } + +mod custom_error { + use super::*; + use thiserror::Error; + + #[nutype( + validate(with = validate_positive_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_positive_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 err: PositivelyOddParseError = "-3".parse::().unwrap_err(); + assert!(matches!( + err, + PositivelyOddParseError::Validate(PositivelyOddError::Negative) + )); + } + + 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"); + } +} From 8387ad07da453155b8bb276ccbb5a73fa20b339d Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Thu, 22 Aug 2024 22:10:06 +0200 Subject: [PATCH 04/16] Fix test_suite/tests/any.rs --- test_suite/tests/any.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/test_suite/tests/any.rs b/test_suite/tests/any.rs index 43366ae..d40a718 100644 --- a/test_suite/tests/any.rs +++ b/test_suite/tests/any.rs @@ -931,7 +931,7 @@ mod custom_error { #[nutype( validate(with = validate_decent_collection, error = DecentCollectionError), - derive(Debug, PartialEq), + derive(Debug, PartialEq, AsRef), )] struct DecentCollection(Vec); @@ -957,13 +957,16 @@ mod custom_error { #[test] fn test_custom_error() { assert_eq!( - Name::try_new("JohnJohnJohnJohnJohn"), - Err(NameError::TooLong) + DecentCollection::try_new(vec![1, 2]), + Err(DecentCollectionError::TooShort) ); - assert_eq!(Name::try_new("Jo"), Err(NameError::TooShort)); + assert_eq!( + DecentCollection::try_new(vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]), + Err(DecentCollectionError::TooLong) + ); - let name = Name::try_new("John").unwrap(); - assert_eq!(name.as_ref(), "John"); + let collection = DecentCollection::try_new(vec![1, 2, 3]).unwrap(); + assert_eq!(collection.as_ref(), &[1, 2, 3]); } } From ffeb0296e92fef8fc857d6daf027282221502a18 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Thu, 22 Aug 2024 22:51:59 +0200 Subject: [PATCH 05/16] Add UI tests for custom error --- nutype_macros/src/common/parse/mod.rs | 8 +++---- .../ui/common/custom_validaiton_no_with.rs | 14 +++++++++++ .../common/custom_validaiton_no_with.stderr | 6 +++++ .../ui/common/custom_validation_mixed.rs | 24 +++++++++++++++++++ .../ui/common/custom_validation_mixed.stderr | 5 ++++ .../common/custom_validation_no_error.stderr | 6 +++++ 6 files changed, 59 insertions(+), 4 deletions(-) create mode 100644 test_suite/tests/ui/common/custom_validaiton_no_with.rs create mode 100644 test_suite/tests/ui/common/custom_validaiton_no_with.stderr create mode 100644 test_suite/tests/ui/common/custom_validation_mixed.rs create mode 100644 test_suite/tests/ui/common/custom_validation_mixed.stderr create mode 100644 test_suite/tests/ui/common/custom_validation_no_error.stderr diff --git a/nutype_macros/src/common/parse/mod.rs b/nutype_macros/src/common/parse/mod.rs index 0ca23c2..c334128 100644 --- a/nutype_macros/src/common/parse/mod.rs +++ b/nutype_macros/src/common/parse/mod.rs @@ -183,11 +183,11 @@ where match (validators.len(), maybe_with, maybe_error) { (0, Some(with), Some(error)) => Ok(RawValidation::Custom { with, error }), (0, Some(_), None) => { - let msg = "`with` attribute must be used with `error` attribute"; + 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(_)) => { - let msg = "`error` attribute must be used with `with` attribute"; + (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) => { @@ -197,7 +197,7 @@ where (_, None, None) => Ok(RawValidation::Standard { validators }), (_, _, _) => { let msg = - "`with` and `error` attributes cannot be used mixed with other validators"; + "`with` and `error` attributes cannot be used mixed with other validators."; Err(syn::Error::new(input.span(), msg)) } } 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,) + | ^ From 32c5c2fd358fca106f9108f3dbce2f084550e7e3 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Fri, 23 Aug 2024 21:43:55 +0200 Subject: [PATCH 06/16] Update README and lib.rs with examples of custom errors --- README.md | 75 ++++++++++++++++++++++++++++++++++---------- nutype/src/lib.rs | 80 ++++++++++++++++++++++++++++++++++++----------- 2 files changed, 121 insertions(+), 34 deletions(-) diff --git a/README.md b/README.md index e5b620a..113e907 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) @@ -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,46 @@ fn is_valid_name(name: &str) -> bool { } ``` +## Custom validation with a custom error type + +If you want to use your own error type and you are ready to write validation logic by yourself, +you can use a combination `with` and `error` attributes: + +```rust +use nutype::nutype; + +// Custom error type. +// It's highly recommended that your error type implements `std::error::Error`, but we skip it +// here. +#[derive(Debug, PartialEq)] +enum NameError { + TooShort, + TooLong, +} + +// Custom validation function. +// Note that return type is `Result<(), NameError>`. +fn validate_name(name: &str) -> Result<(), NameError> { + if name.len() < 3 { + Err(NameError::TooShort) + } else if name.len() > 10 { + Err(NameError::TooLong) + } else { + Ok(()) + } +} + +#[nutype( + validate(with = validate_name, error = NameError), + derive(Debug, PartialEq), +)] +struct Name(String); + +``` + +Note that it's important that type provided with `error` attribute matches the error variant in +the type returned by the validation function. + ## Recipes ### Derive `Default` diff --git a/nutype/src/lib.rs b/nutype/src/lib.rs index e4008d0..56ae940 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 +//! +//! If you want to use your own error type and you are ready to write validation logic by yourself, +//! you can use a combination `with` and `error` attributes: +//! +//! ``` +//! # mod wrapper_module { +//! use nutype::nutype; +//! +//! // Custom error type. +//! // It's highly recommended that your error type implements `std::error::Error`, but we skip it +//! // here. +//! #[derive(Debug, PartialEq)] +//! enum NameError { +//! TooShort, +//! TooLong, +//! } +//! +//! // Custom validation function. +//! // Note that return type is `Result<(), NameError>`. +//! fn validate_name(name: &str) -> Result<(), NameError> { +//! if name.len() < 3 { +//! Err(NameError::TooShort) +//! } else if name.len() > 10 { +//! Err(NameError::TooLong) +//! } else { +//! Ok(()) +//! } +//! } +//! +//! #[nutype( +//! validate(with = validate_name, error = NameError), +//! derive(Debug, PartialEq), +//! )] +//! struct Name(String); +//! +//! # } +//! ``` +//! Note that it's important that type provided with `error` attribute matches the error variant in +//! the type returned by the validation function. +//! //! ## Recipes //! //! ### Derive `Default` From 0de5af411d3f306446b79104ac2a3e348cb70527 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Fri, 23 Aug 2024 21:53:12 +0200 Subject: [PATCH 07/16] Add note on custom errors to the CHANGELOG --- CHANGELOG.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d93bf51..6c684cc 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 using the `error` and `with` attributes. +* [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 or higher). This change is potentially is a breaking change, since the code that uses regex may stop compiling on the older versions of Rust, because it generates code that uses `std::sync::LazyLock` now. You can still explicitly use `lazy_static` to work around, if you cannot update to the newer versions of Rust. +* [BREAKING] Fallible `::new()` constructor is fully replaced with `::try_new()`. ### v0.4.3 - 2024-07-06 From 7e06739087aa9cf7352d95e99dbb0326b3a25c53 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 14:29:31 +0200 Subject: [PATCH 08/16] Improve wording in the CHANGELOG --- CHANGELOG.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6c684cc..db8194e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,8 +1,8 @@ ### v0.5.0 - 2024-xx-xx -* [FEATURE] Added support for custom error types and validation functions using the `error` and `with` attributes. -* [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 or higher). This change is potentially is a breaking change, since the code that uses regex may stop compiling on the older versions of Rust, because it generates code that uses `std::sync::LazyLock` now. You can still explicitly use `lazy_static` to work around, if you cannot update to the newer versions of Rust. -* [BREAKING] Fallible `::new()` constructor is fully replaced with `::try_new()`. +- **[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 From 71c59e137d55afd7a6322e770e92110848380b78 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 14:30:24 +0200 Subject: [PATCH 09/16] Format Cargo.toml --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 643bd55..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/custom_error", + "examples/string_arbitrary", + "examples/any_generics", + "examples/custom_error", ] From 1cf4a54374d209c7d1b8915e4a3ffaceae71f748 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 14:37:41 +0200 Subject: [PATCH 10/16] Improve wording in README and lib.rs --- README.md | 19 +++++++++---------- nutype/src/lib.rs | 20 ++++++++++---------- 2 files changed, 19 insertions(+), 20 deletions(-) diff --git a/README.md b/README.md index 113e907..2da3c68 100644 --- a/README.md +++ b/README.md @@ -310,23 +310,23 @@ fn is_valid_name(name: &str) -> bool { ## Custom validation with a custom error type -If you want to use your own error type and you are ready to write validation logic by yourself, -you can use a combination `with` and `error` attributes: +To define your own error type and implement custom validation logic, you can combine the `with` and `error` attributes: ```rust use nutype::nutype; -// Custom error type. -// It's highly recommended that your error type implements `std::error::Error`, but we skip it -// here. +// 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, } -// Custom validation function. -// Note that return type is `Result<(), NameError>`. +// 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) @@ -337,16 +337,15 @@ fn validate_name(name: &str) -> Result<(), NameError> { } } +// Define a newtype `Name` with custom validation logic and custom error. #[nutype( validate(with = validate_name, error = NameError), derive(Debug, PartialEq), )] struct Name(String); - ``` -Note that it's important that type provided with `error` attribute matches the error variant in -the type returned by the validation function. +It's important to ensure that the type specified in the `error` attribute matches the error type returned by the validation function. ## Recipes diff --git a/nutype/src/lib.rs b/nutype/src/lib.rs index 56ae940..aba8198 100644 --- a/nutype/src/lib.rs +++ b/nutype/src/lib.rs @@ -357,24 +357,24 @@ //! //! ## Custom validation with a custom error type //! -//! If you want to use your own error type and you are ready to write validation logic by yourself, -//! you can use a combination `with` and `error` attributes: +//! 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; //! -//! // Custom error type. -//! // It's highly recommended that your error type implements `std::error::Error`, but we skip it -//! // here. +//! // 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, //! } //! -//! // Custom validation function. -//! // Note that return type is `Result<(), NameError>`. +//! // 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) @@ -385,16 +385,16 @@ //! } //! } //! +//! // Define a newtype `Name` with custom validation logic and custom error. //! #[nutype( //! validate(with = validate_name, error = NameError), //! derive(Debug, PartialEq), //! )] //! struct Name(String); -//! //! # } //! ``` -//! Note that it's important that type provided with `error` attribute matches the error variant in -//! the type returned by the validation function. +//! +//! It's important to ensure that the type specified in the `error` attribute matches the error type returned by the validation function. //! //! ## Recipes //! From 091441972fffa5156777fb2ba5580025e9d976ad Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 14:50:49 +0200 Subject: [PATCH 11/16] Fix anchor link in README --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 2da3c68..80c3201 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) | (errors)[#custom-validation-with-a-custom-error-type]) +* [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 From d49ba9d00a2cbb69283a67b67a5af4c947c23a14 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 14:52:53 +0200 Subject: [PATCH 12/16] Update README --- README.md | 2 -- 1 file changed, 2 deletions(-) diff --git a/README.md b/README.md index 80c3201..642f4c7 100644 --- a/README.md +++ b/README.md @@ -313,8 +313,6 @@ fn is_valid_name(name: &str) -> bool { To define your own error type and implement custom validation logic, you can combine the `with` and `error` attributes: ```rust -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. From f31d01bdcc2ad7a30a4052b9136f778b555548e8 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 15:09:58 +0200 Subject: [PATCH 13/16] Rename validate_positive_odd -> validate_positively_odd --- examples/custom_error/src/main.rs | 4 ++-- test_suite/tests/integer.rs | 12 ++---------- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/examples/custom_error/src/main.rs b/examples/custom_error/src/main.rs index da07ee8..7884d37 100644 --- a/examples/custom_error/src/main.rs +++ b/examples/custom_error/src/main.rs @@ -2,7 +2,7 @@ use nutype::nutype; use thiserror::Error; #[nutype( - validate(with = validate_positive_odd, error = PositivelyOddError), + validate(with = validate_positively_odd, error = PositivelyOddError), derive(Debug, FromStr), )] struct PositivelyOdd(i32); @@ -16,7 +16,7 @@ enum PositivelyOddError { Even, } -fn validate_positive_odd(value: &i32) -> Result<(), PositivelyOddError> { +fn validate_positively_odd(value: &i32) -> Result<(), PositivelyOddError> { if *value < 0 { return Err(PositivelyOddError::Negative); } diff --git a/test_suite/tests/integer.rs b/test_suite/tests/integer.rs index 4fb60d4..eea1c59 100644 --- a/test_suite/tests/integer.rs +++ b/test_suite/tests/integer.rs @@ -838,7 +838,7 @@ mod custom_error { use thiserror::Error; #[nutype( - validate(with = validate_positive_odd, error = PositivelyOddError), + validate(with = validate_positively_odd, error = PositivelyOddError), derive(Debug, FromStr), )] struct PositivelyOdd(i32); @@ -852,7 +852,7 @@ mod custom_error { Even, } - fn validate_positive_odd(value: &i32) -> Result<(), PositivelyOddError> { + fn validate_positively_odd(value: &i32) -> Result<(), PositivelyOddError> { if *value < 0 { return Err(PositivelyOddError::Negative); } @@ -876,14 +876,6 @@ mod custom_error { assert_eq!(err, PositivelyOddError::Even); } - { - let err: PositivelyOddParseError = "-3".parse::().unwrap_err(); - assert!(matches!( - err, - PositivelyOddParseError::Validate(PositivelyOddError::Negative) - )); - } - let podd: PositivelyOdd = PositivelyOdd::try_new(3).unwrap(); assert_eq!(podd.into_inner(), 3); } From 944b4d1e0a18e1e77904ddd0aa18dff3677964d5 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 16:00:24 +0200 Subject: [PATCH 14/16] Add docs on Validaiton --- nutype_macros/src/common/gen/mod.rs | 4 ++-- nutype_macros/src/common/models.rs | 7 +++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/nutype_macros/src/common/gen/mod.rs b/nutype_macros/src/common/gen/mod.rs index 3b790bd..01da127 100644 --- a/nutype_macros/src/common/gen/mod.rs +++ b/nutype_macros/src/common/gen/mod.rs @@ -410,7 +410,7 @@ pub trait GenerateNewtype { &traits, ); - let maybe_error_type_name = match &guard { + 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. @@ -425,7 +425,7 @@ pub trait GenerateNewtype { vis, &type_name, &module_name, - maybe_error_type_name, + maybe_reimported_error_type_name, maybe_parse_error_type_name.as_ref(), ); diff --git a/nutype_macros/src/common/models.rs b/nutype_macros/src/common/models.rs index 9753967..de57cd9 100644 --- a/nutype_macros/src/common/models.rs +++ b/nutype_macros/src/common/models.rs @@ -225,11 +225,18 @@ pub enum Guard { #[derive(Debug)] pub enum Validation { 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, }, } From 940de6a42d1f6a28287fb21f98e1a59ccd16a7f8 Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 16:17:16 +0200 Subject: [PATCH 15/16] Cleanup nutype_macros/src/common/validate.rs --- nutype_macros/src/common/validate.rs | 47 ++++++++++++++-------------- 1 file changed, 23 insertions(+), 24 deletions(-) diff --git a/nutype_macros/src/common/validate.rs b/nutype_macros/src/common/validate.rs index f6792f8..696efca 100644 --- a/nutype_macros/src/common/validate.rs +++ b/nutype_macros/src/common/validate.rs @@ -22,34 +22,33 @@ pub fn validate_guard( validation: maybe_raw_validation, } = raw_guard; - //let validators = validate_validators(raw_validators)?; let sanitizers = validate_sanitizers(raw_sanitizers)?; - if let Some(raw_validation) = maybe_raw_validation { - 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, - } + 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, - } + } + RawValidation::Custom { with, error } => { + let error_type_name = error; + Validation::Custom { + with, + error_type_name, } - }; - Ok(Guard::WithValidation { - sanitizers, - validation, - }) - } else { - Ok(Guard::WithoutValidation { sanitizers }) - } + } + }; + Ok(Guard::WithValidation { + sanitizers, + validation, + }) } pub fn validate_duplicates( From 9be4ce60d952e43e2ccdb065eaa5895d3b96e69f Mon Sep 17 00:00:00 2001 From: Serhii Potapov Date: Sat, 24 Aug 2024 16:21:30 +0200 Subject: [PATCH 16/16] Add a bit of doc on ExtraValidateAttr --- nutype_macros/src/common/parse/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/nutype_macros/src/common/parse/mod.rs b/nutype_macros/src/common/parse/mod.rs index c334128..b6973d0 100644 --- a/nutype_macros/src/common/parse/mod.rs +++ b/nutype_macros/src/common/parse/mod.rs @@ -78,6 +78,8 @@ enum ValidateAttr { Extra(ExtraValidateAttr), } +/// Non standard (custom) validation attributes. +/// Responsible for parsing `error` and `with` attributes. #[derive(Debug, Kinded)] #[kinded(display = "snake_case")] enum ExtraValidateAttr { @@ -108,6 +110,7 @@ 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.