diff --git a/book/src/getting-started.md b/book/src/getting-started.md index 88c58eb..115124f 100644 --- a/book/src/getting-started.md +++ b/book/src/getting-started.md @@ -62,7 +62,15 @@ Call the `validate_sync` method on the data structure: ```rust # extern crate fortifier; -use fortifier::{EmailAddressError, LengthError, Validate, ValidationErrors}; +# +use fortifier::{ + EmailAddressError, + EmailAddressErrorCode, + LengthError, + LengthErrorCode, + Validate, + ValidationErrors, +}; #[derive(Validate)] struct CreateUser { @@ -90,10 +98,13 @@ fn main() { data.validate_sync(), Err(ValidationErrors::from_iter([ CreateUserValidationError::EmailAddress( - EmailAddressError::MissingSeparator {}, + EmailAddressError::MissingSeparator { + code: EmailAddressErrorCode, + }, ), CreateUserValidationError::Name( LengthError::Min { + code: LengthErrorCode, min: 1, length: 0, } diff --git a/packages/fortifier-macros/tests/integrations/serde_pass.rs b/packages/fortifier-macros/tests/integrations/serde_pass.rs index ec2ecad..8b3a99a 100644 --- a/packages/fortifier-macros/tests/integrations/serde_pass.rs +++ b/packages/fortifier-macros/tests/integrations/serde_pass.rs @@ -31,19 +31,19 @@ fn main() { json!([ { "path": "name", - // "code": "length", + "code": "length", "subcode": "min", "min": 1, "length": 0 }, { "path": "emailAddresses", - // "code": "nested", + "code": "nested", "errors": [ { "index": 0, "path": "emailAddress", - // "code": "emailAddress", + "code": "emailAddress", "subcode": "missingSeparator" } ] diff --git a/packages/fortifier-macros/tests/validations/length/options_pass.rs b/packages/fortifier-macros/tests/validations/length/options_pass.rs index 79e19a4..036cc3f 100644 --- a/packages/fortifier-macros/tests/validations/length/options_pass.rs +++ b/packages/fortifier-macros/tests/validations/length/options_pass.rs @@ -1,4 +1,4 @@ -use fortifier::{LengthError, Validate, ValidationErrors}; +use fortifier::{LengthError, LengthErrorCode, Validate, ValidationErrors}; #[derive(Validate)] struct LengthData<'a> { @@ -24,12 +24,25 @@ fn main() { data.validate_sync(), Err(ValidationErrors::from_iter([ LengthDataValidationError::Equal(LengthError::Equal { + code: LengthErrorCode, equal: 2, length: 1 }), - LengthDataValidationError::Min(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::Max(LengthError::Max { max: 4, length: 5 }), - LengthDataValidationError::MinMax(LengthError::Max { max: 4, length: 6 }) + LengthDataValidationError::Min(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::Max(LengthError::Max { + code: LengthErrorCode, + max: 4, + length: 5 + }), + LengthDataValidationError::MinMax(LengthError::Max { + code: LengthErrorCode, + max: 4, + length: 6 + }) ])) ); } diff --git a/packages/fortifier-macros/tests/validations/length/types_pass.rs b/packages/fortifier-macros/tests/validations/length/types_pass.rs index 4072e1d..52811a3 100644 --- a/packages/fortifier-macros/tests/validations/length/types_pass.rs +++ b/packages/fortifier-macros/tests/validations/length/types_pass.rs @@ -1,6 +1,6 @@ use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, LinkedList, VecDeque}; -use fortifier::{LengthError, Validate, ValidationErrors}; +use fortifier::{LengthError, LengthErrorCode, Validate, ValidationErrors}; use indexmap::{IndexMap, IndexSet}; #[derive(Validate)] @@ -53,19 +53,71 @@ fn main() { assert_eq!( data.validate_sync(), Err(ValidationErrors::from_iter([ - LengthDataValidationError::Str(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::String(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::Array(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::Slice(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::BTreeMap(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::BTreeSet(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::HashMap(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::HashSet(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::IndexMap(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::IndexSet(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::LinkedList(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::Vec(LengthError::Min { min: 1, length: 0 }), - LengthDataValidationError::VecDeque(LengthError::Min { min: 1, length: 0 }), + LengthDataValidationError::Str(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::String(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::Array(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::Slice(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::BTreeMap(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::BTreeSet(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::HashMap(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::HashSet(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::IndexMap(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::IndexSet(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::LinkedList(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::Vec(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), + LengthDataValidationError::VecDeque(LengthError::Min { + code: LengthErrorCode, + min: 1, + length: 0 + }), ])) ); } diff --git a/packages/fortifier-macros/tests/validations/phone-number/options_pass.rs b/packages/fortifier-macros/tests/validations/phone-number/options_pass.rs index dfd103e..17c5ec6 100644 --- a/packages/fortifier-macros/tests/validations/phone-number/options_pass.rs +++ b/packages/fortifier-macros/tests/validations/phone-number/options_pass.rs @@ -1,4 +1,6 @@ -use fortifier::{PhoneNumberCountry, PhoneNumberError, Validate, ValidationErrors}; +use fortifier::{ + PhoneNumberCountry, PhoneNumberError, PhoneNumberErrorCode, Validate, ValidationErrors, +}; use phonenumber::ParseError; #[derive(Validate)] @@ -29,6 +31,7 @@ fn main() { )), PhoneNumberDataValidationError::AllowedCountries( PhoneNumberError::DisallowedCountryCode { + code: PhoneNumberErrorCode, allowed: vec![PhoneNumberCountry::GB], value: Some(PhoneNumberCountry::NL) } diff --git a/packages/fortifier-macros/tests/validations/phone-number/types_pass.rs b/packages/fortifier-macros/tests/validations/phone-number/types_pass.rs index 9c23fda..b78609c 100644 --- a/packages/fortifier-macros/tests/validations/phone-number/types_pass.rs +++ b/packages/fortifier-macros/tests/validations/phone-number/types_pass.rs @@ -1,6 +1,8 @@ use std::str::FromStr; -use fortifier::{PhoneNumberCountry, PhoneNumberError, Validate, ValidationErrors}; +use fortifier::{ + PhoneNumberCountry, PhoneNumberError, PhoneNumberErrorCode, Validate, ValidationErrors, +}; use phonenumber::{ParseError, PhoneNumber}; #[derive(Validate)] @@ -28,6 +30,7 @@ fn main() { )), PhoneNumberDataValidationError::String(PhoneNumberError::from(ParseError::TooShortNsn)), PhoneNumberDataValidationError::PhoneNumber(PhoneNumberError::DisallowedCountryCode { + code: PhoneNumberErrorCode, allowed: vec![PhoneNumberCountry::NL], value: Some(PhoneNumberCountry::GB), }) diff --git a/packages/fortifier/src/error_code.rs b/packages/fortifier/src/error_code.rs new file mode 100644 index 0000000..ffc88b7 --- /dev/null +++ b/packages/fortifier/src/error_code.rs @@ -0,0 +1,67 @@ +/// Implement an error code. +#[macro_export] +macro_rules! error_code { + ($name:ident, $code:literal) => { + const CODE: &str = $code; + + /// Email address error code. + #[derive(Eq, PartialEq)] + pub struct $name; + + impl Default for $name { + fn default() -> Self { + Self + } + } + + impl ::std::ops::Deref for $name { + type Target = str; + + fn deref(&self) -> &Self::Target { + CODE + } + } + + impl ::std::fmt::Debug for $name { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + ::std::fmt::Debug::fmt(&**self, f) + } + } + + #[cfg(feature = "serde")] + impl<'de> ::serde::Deserialize<'de> for $name { + fn deserialize(deserializer: D) -> Result + where + D: ::serde::Deserializer<'de>, + { + deserializer + .deserialize_any($crate::integrations::serde::MustBeStrVisitor(CODE)) + .map(|()| Self) + } + } + + #[cfg(feature = "serde")] + impl ::serde::Serialize for $name { + fn serialize(&self, serializer: S) -> Result + where + S: ::serde::Serializer, + { + serializer.serialize_str(CODE) + } + } + + #[cfg(feature = "utoipa")] + impl ::utoipa::PartialSchema for $name { + fn schema() -> ::utoipa::openapi::RefOr<::utoipa::openapi::schema::Schema> { + ::utoipa::openapi::schema::ObjectBuilder::new() + .schema_type(::utoipa::openapi::schema::Type::String) + .enum_values(Some([CODE])) + .build() + .into() + } + } + + #[cfg(feature = "utoipa")] + impl ::utoipa::ToSchema for $name {} + }; +} diff --git a/packages/fortifier/src/integrations/serde.rs b/packages/fortifier/src/integrations/serde.rs index 349971e..81398e0 100644 --- a/packages/fortifier/src/integrations/serde.rs +++ b/packages/fortifier/src/integrations/serde.rs @@ -1,5 +1,9 @@ //! Serde utilities +use std::fmt; + +use serde::de::{Error, Unexpected, Visitor}; + /// Deserialize and serialize with `errors` field. pub mod errors { use serde::{Deserialize, Deserializer, Serialize, Serializer}; @@ -27,9 +31,38 @@ pub mod errors { { #[derive(Serialize)] struct Wrapper<'a, T> { + code: &'static str, errors: &'a T, } - Wrapper { errors: value }.serialize(serializer) + Wrapper { + code: "nested", + errors: value, + } + .serialize(serializer) + } +} + +/// Serde visitor for a static string. +/// +/// Based on `MustBeStrVisitor` from [`monostate`](https://crates.io/crates/monostate). +pub struct MustBeStrVisitor(pub &'static str); + +impl<'de> Visitor<'de> for MustBeStrVisitor { + type Value = (); + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "string {:?}", self.0) + } + + fn visit_str(self, v: &str) -> Result + where + E: Error, + { + if v == self.0 { + Ok(()) + } else { + Err(E::invalid_value(Unexpected::Str(v), &self)) + } } } diff --git a/packages/fortifier/src/lib.rs b/packages/fortifier/src/lib.rs index bc51498..303f7a4 100644 --- a/packages/fortifier/src/lib.rs +++ b/packages/fortifier/src/lib.rs @@ -3,6 +3,7 @@ //! Fortifier. mod error; +mod error_code; mod integrations; mod validate; mod validations; diff --git a/packages/fortifier/src/validations/email_address.rs b/packages/fortifier/src/validations/email_address.rs index 63aa54a..b22c342 100644 --- a/packages/fortifier/src/validations/email_address.rs +++ b/packages/fortifier/src/validations/email_address.rs @@ -8,6 +8,10 @@ use std::{ use email_address::EmailAddress; pub use email_address::Options as EmailAddressOptions; +use crate::error_code; + +error_code!(EmailAddressErrorCode, "emailAddress"); + /// Email validation error. #[derive(Debug, Eq, PartialEq)] #[cfg_attr( @@ -23,102 +27,170 @@ pub use email_address::Options as EmailAddressOptions; pub enum EmailAddressError { /// Invalid character error. InvalidCharacter { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Missing separator error. MissingSeparator { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Locale part empty error. LocalPartEmpty { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Local part too long error. LocalPartTooLong { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Domain empty error. DomainEmpty { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Domain too long error. DomainTooLong { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Subdomain empty error. SubDomainEmpty { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Subdomain too long error. SubDomainTooLong { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Domain too few error. DomainTooFew { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Domain invalid separator error. DomainInvalidSeparator { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Unbalanced quotes error. UnbalancedQuotes { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid comment error. InvalidComment { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid IP Address error. InvalidIPAddress { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Unsupported domain literal error. UnsupportedDomainLiteral { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Unsupported display name error. UnsupportedDisplayName { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Missing display name error. MissingDisplayName { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Missing end bracket error. MissingEndBracket { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: EmailAddressErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, @@ -127,72 +199,108 @@ pub enum EmailAddressError { impl From for EmailAddressError { fn from(value: email_address::Error) -> Self { + let code = EmailAddressErrorCode; + match value { email_address::Error::InvalidCharacter => Self::InvalidCharacter { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::MissingSeparator => Self::MissingSeparator { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::LocalPartEmpty => Self::LocalPartEmpty { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::LocalPartTooLong => Self::LocalPartTooLong { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::DomainEmpty => Self::DomainEmpty { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::DomainTooLong => Self::DomainTooLong { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::SubDomainEmpty => Self::SubDomainEmpty { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::SubDomainTooLong => Self::SubDomainTooLong { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::DomainTooFew => Self::DomainTooFew { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::DomainInvalidSeparator => Self::DomainInvalidSeparator { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::UnbalancedQuotes => Self::UnbalancedQuotes { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::InvalidComment => Self::InvalidComment { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::InvalidIPAddress => Self::InvalidIPAddress { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::UnsupportedDomainLiteral => Self::UnsupportedDomainLiteral { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::UnsupportedDisplayName => Self::UnsupportedDisplayName { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::MissingDisplayName => Self::MissingDisplayName { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, email_address::Error::MissingEndBracket => Self::MissingEndBracket { + code, + #[cfg(feature = "message")] message: "".to_owned(), }, diff --git a/packages/fortifier/src/validations/length.rs b/packages/fortifier/src/validations/length.rs index 51ba18a..85b5f31 100644 --- a/packages/fortifier/src/validations/length.rs +++ b/packages/fortifier/src/validations/length.rs @@ -10,6 +10,10 @@ use std::{ #[cfg(feature = "indexmap")] use indexmap::{IndexMap, IndexSet}; +use crate::error_code; + +error_code!(LengthErrorCode, "length"); + /// Length validation error. #[derive(Debug, Eq, PartialEq)] #[cfg_attr( @@ -31,6 +35,10 @@ pub enum LengthError { /// The actual length. length: T, + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: LengthErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, @@ -43,6 +51,10 @@ pub enum LengthError { /// The actual length. length: T, + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: LengthErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, @@ -55,6 +67,10 @@ pub enum LengthError { /// The length. length: T, + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: LengthErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, @@ -88,6 +104,7 @@ where return Err(LengthError::Equal { equal, length, + code: LengthErrorCode, #[cfg(feature = "message")] message, }); @@ -102,6 +119,7 @@ where return Err(LengthError::Min { min, length, + code: LengthErrorCode, #[cfg(feature = "message")] message, }); @@ -116,6 +134,7 @@ where return Err(LengthError::Max { max, length, + code: LengthErrorCode, #[cfg(feature = "message")] message, }); @@ -232,6 +251,8 @@ mod tests { #[cfg(feature = "indexmap")] use indexmap::{IndexMap, IndexSet}; + use crate::LengthErrorCode; + use super::{LengthError, ValidateLength}; #[test] @@ -310,6 +331,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -319,6 +341,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -328,6 +351,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -337,6 +361,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -346,6 +371,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -356,6 +382,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -366,6 +393,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -375,6 +403,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -384,6 +413,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -393,6 +423,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -402,6 +433,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -411,6 +443,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -420,6 +453,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -429,6 +463,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -441,6 +476,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -450,6 +486,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -461,6 +498,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -472,6 +510,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -482,6 +521,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -491,6 +531,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -502,6 +543,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -511,6 +553,7 @@ mod tests { Err(LengthError::Equal { equal: 2, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is not equal to required length 2".to_owned(), }) @@ -524,6 +567,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -533,6 +577,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -542,6 +587,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -551,6 +597,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -560,6 +607,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -570,6 +618,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -580,6 +629,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -589,6 +639,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -598,6 +649,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -607,6 +659,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -616,6 +669,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -625,6 +679,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -634,6 +689,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -643,6 +699,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -655,6 +712,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -664,6 +722,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -675,6 +734,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -686,6 +746,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -696,6 +757,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -705,6 +767,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -716,6 +779,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -725,6 +789,7 @@ mod tests { Err(LengthError::Min { min: 3, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is less than minimum length 3".to_owned(), }) @@ -738,6 +803,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -747,6 +813,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -756,6 +823,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -765,6 +833,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -774,6 +843,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -784,6 +854,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -794,6 +865,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -803,6 +875,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -812,6 +885,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -821,6 +895,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -830,6 +905,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -839,6 +915,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -848,6 +925,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -857,6 +935,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -869,6 +948,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -878,6 +958,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -889,6 +970,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -900,6 +982,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -910,6 +993,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -919,6 +1003,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -930,6 +1015,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) @@ -939,6 +1025,7 @@ mod tests { Err(LengthError::Max { max: 0, length: 1, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 1 is greater than maximum length 0".to_owned(), }) diff --git a/packages/fortifier/src/validations/phone_number.rs b/packages/fortifier/src/validations/phone_number.rs index 603751b..f105719 100644 --- a/packages/fortifier/src/validations/phone_number.rs +++ b/packages/fortifier/src/validations/phone_number.rs @@ -8,6 +8,10 @@ use std::{ pub use phonenumber::country::Id as PhoneNumberCountry; use phonenumber::{ParseError, PhoneNumber}; +use crate::error_code; + +error_code!(PhoneNumberErrorCode, "phoneNumber"); + /// Phone number validation error. #[derive(Debug, Eq, PartialEq)] #[cfg_attr( @@ -23,42 +27,70 @@ use phonenumber::{ParseError, PhoneNumber}; pub enum PhoneNumberError { /// No number error. NoNumber { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: PhoneNumberErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid country error. InvalidCountryCode { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: PhoneNumberErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Too short after IDD error. TooShortAfterIdd { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: PhoneNumberErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Too short NSN error. TooShortNsn { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: PhoneNumberErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Too long error. TooLong { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: PhoneNumberErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Malformed integer error. MalformedInteger { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: PhoneNumberErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Disallowed country code error. DisallowedCountryCode { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: PhoneNumberErrorCode, + /// Allowed country codes. #[cfg_attr(feature = "utoipa", schema(value_type = Vec))] allowed: Vec, @@ -77,28 +109,42 @@ pub enum PhoneNumberError { impl From for PhoneNumberError { fn from(value: ParseError) -> Self { + let code = PhoneNumberErrorCode; + match value { ParseError::NoNumber => Self::NoNumber { + code, + #[cfg(feature = "message")] message: "no number".to_owned(), }, ParseError::InvalidCountryCode => Self::InvalidCountryCode { + code, + #[cfg(feature = "message")] message: "invalid country code".to_owned(), }, ParseError::TooShortAfterIdd => Self::TooShortAfterIdd { + code, + #[cfg(feature = "message")] message: "too short after IDD".to_owned(), }, ParseError::TooShortNsn => Self::TooShortNsn { + code, + #[cfg(feature = "message")] message: "too short NSN".to_owned(), }, ParseError::TooLong => Self::TooLong { + code, + #[cfg(feature = "message")] message: "too long".to_owned(), }, ParseError::MalformedInteger(_) => Self::MalformedInteger { + code, + #[cfg(feature = "message")] message: "malformed integer".to_owned(), }, @@ -142,6 +188,7 @@ pub trait ValidatePhoneNumber { return Err(PhoneNumberError::DisallowedCountryCode { allowed: allowed_countries, value: Some(country), + code: PhoneNumberErrorCode, #[cfg(feature = "message")] message, }); @@ -161,6 +208,7 @@ pub trait ValidatePhoneNumber { return Err(PhoneNumberError::DisallowedCountryCode { allowed: allowed_countries, value: None, + code: PhoneNumberErrorCode, #[cfg(feature = "message")] message, }); @@ -241,6 +289,8 @@ mod tests { use phonenumber::{ParseError, PhoneNumber}; + use crate::PhoneNumberErrorCode; + use super::{PhoneNumberCountry, PhoneNumberError, ValidatePhoneNumber}; #[test] @@ -368,6 +418,7 @@ mod tests { Err(PhoneNumberError::DisallowedCountryCode { allowed: vec![PhoneNumberCountry::NL], value: Some(PhoneNumberCountry::GB), + code: PhoneNumberErrorCode, #[cfg(feature = "message")] message: "country code `GB` is not allowed, must be one of `NL`".to_owned() }) diff --git a/packages/fortifier/src/validations/regex.rs b/packages/fortifier/src/validations/regex.rs index f030254..2ec626b 100644 --- a/packages/fortifier/src/validations/regex.rs +++ b/packages/fortifier/src/validations/regex.rs @@ -7,6 +7,8 @@ use std::{ use regex::Regex; +use crate::error_code; + /// Convert to a regular expression. pub trait AsRegex { /// Convert to a regular expression. @@ -34,6 +36,8 @@ where } } +error_code!(RegexErrorCode, "regex"); + /// Regular expression validation error. #[derive(Debug, Eq, PartialEq)] #[cfg_attr( @@ -43,15 +47,20 @@ where )] #[cfg_attr(feature = "utoipa", derive(utoipa::ToSchema))] pub struct RegexError { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: RegexErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, } -#[cfg_attr(not(feature = "message"), expect(clippy::derivable_impls))] impl Default for RegexError { fn default() -> Self { Self { + code: RegexErrorCode, + #[cfg(feature = "message")] message: "value does not match regular expression".to_owned(), } @@ -148,6 +157,8 @@ mod tests { use regex::Regex; + use crate::RegexErrorCode; + use super::{RegexError, ValidateRegex}; static REGEX: LazyLock = LazyLock::new(|| Regex::new(r"[0-9]{4}").expect("valid regex")); @@ -207,6 +218,7 @@ mod tests { assert_eq!( Box::new("123").validate_regex(®EX), Err(RegexError { + code: RegexErrorCode, #[cfg(feature = "message")] message: "value does not match regular expression".to_owned(), }) diff --git a/packages/fortifier/src/validations/url.rs b/packages/fortifier/src/validations/url.rs index 451b8b9..19836a0 100644 --- a/packages/fortifier/src/validations/url.rs +++ b/packages/fortifier/src/validations/url.rs @@ -7,6 +7,10 @@ use std::{ use url::{ParseError, Url}; +use crate::error_code; + +error_code!(UrlErrorCode, "url"); + /// URL validation error. #[derive(Debug, Eq, PartialEq)] #[cfg_attr( @@ -22,66 +26,110 @@ use url::{ParseError, Url}; pub enum UrlError { /// Empty host error. EmptyHost { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid international domain name error. IdnaError { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid port error. InvalidPort { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid IPv4 address error. InvalidIpv4Address { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid IPv6 address error. InvalidIpv6Address { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Invalid domain character error. InvalidDomainCharacter { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Relative URL without base error. RelativeUrlWithoutBase { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Relative URL with cannot-be-a-base base error. RelativeUrlWithCannotBeABaseBase { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Set host on cannot-be-a-base URL error. SetHostOnCannotBeABaseUrl { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Overflow error. Overflow { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, }, /// Unknown error. Unknown { + /// The error code. + #[cfg_attr(feature = "serde", serde(default))] + code: UrlErrorCode, + /// A human-readable error message. #[cfg(feature = "message")] message: String, @@ -90,51 +138,75 @@ pub enum UrlError { impl From for UrlError { fn from(value: ParseError) -> Self { + let code = UrlErrorCode; + match value { ParseError::EmptyHost => UrlError::EmptyHost { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::IdnaError => UrlError::IdnaError { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::InvalidPort => UrlError::InvalidPort { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::InvalidIpv4Address => UrlError::InvalidIpv4Address { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::InvalidIpv6Address => UrlError::InvalidIpv6Address { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::InvalidDomainCharacter => UrlError::InvalidDomainCharacter { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::RelativeUrlWithoutBase => UrlError::RelativeUrlWithoutBase { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::RelativeUrlWithCannotBeABaseBase => { UrlError::RelativeUrlWithCannotBeABaseBase { + code, + #[cfg(feature = "message")] message: value.to_string(), } } ParseError::SetHostOnCannotBeABaseUrl => UrlError::SetHostOnCannotBeABaseUrl { + code, + #[cfg(feature = "message")] message: value.to_string(), }, ParseError::Overflow => UrlError::Overflow { + code, + #[cfg(feature = "message")] message: value.to_string(), }, #[cfg_attr(not(feature = "message"), allow(unused_variables))] value => UrlError::Overflow { + code, + #[cfg(feature = "message")] message: value.to_string(), }, @@ -233,6 +305,8 @@ mod tests { use url::{ParseError, Url}; + use crate::UrlErrorCode; + use super::{UrlError, ValidateUrl}; #[test] @@ -308,6 +382,7 @@ mod tests { assert_eq!( Box::new("http://").validate_url(), Err(UrlError::EmptyHost { + code: UrlErrorCode, #[cfg(feature = "message")] message: "empty host".to_owned(), }) diff --git a/packages/fortifier/tests/serde.rs b/packages/fortifier/tests/serde.rs index c34b2e2..aa57b34 100644 --- a/packages/fortifier/tests/serde.rs +++ b/packages/fortifier/tests/serde.rs @@ -1,6 +1,8 @@ #![cfg(feature = "serde")] -use fortifier::{EmailAddressError, LengthError, RegexError, UrlError, ValidationErrors}; +use fortifier::{ + EmailAddressError, LengthError, LengthErrorCode, RegexError, UrlError, ValidationErrors, +}; use pretty_assertions::assert_eq; use serde::{Deserialize, Serialize}; use serde_json::{Value, json}; @@ -24,6 +26,7 @@ fn setup() -> (ValidationErrors, Value) { TestError::Length(LengthError::Equal { equal: 1, length: 2, + code: LengthErrorCode, #[cfg(feature = "message")] message: "length 2 is not equal to required length 1".to_owned(), }),