Skip to content

Commit 8f220dc

Browse files
feat: add error codes (#35)
1 parent 60fe6e2 commit 8f220dc

15 files changed

Lines changed: 547 additions & 28 deletions

File tree

book/src/getting-started.md

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,15 @@ Call the `validate_sync` method on the data structure:
6262

6363
```rust
6464
# extern crate fortifier;
65-
use fortifier::{EmailAddressError, LengthError, Validate, ValidationErrors};
65+
#
66+
use fortifier::{
67+
EmailAddressError,
68+
EmailAddressErrorCode,
69+
LengthError,
70+
LengthErrorCode,
71+
Validate,
72+
ValidationErrors,
73+
};
6674

6775
#[derive(Validate)]
6876
struct CreateUser {
@@ -90,10 +98,13 @@ fn main() {
9098
data.validate_sync(),
9199
Err(ValidationErrors::from_iter([
92100
CreateUserValidationError::EmailAddress(
93-
EmailAddressError::MissingSeparator {},
101+
EmailAddressError::MissingSeparator {
102+
code: EmailAddressErrorCode,
103+
},
94104
),
95105
CreateUserValidationError::Name(
96106
LengthError::Min {
107+
code: LengthErrorCode,
97108
min: 1,
98109
length: 0,
99110
}

packages/fortifier-macros/tests/integrations/serde_pass.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,19 @@ fn main() {
3131
json!([
3232
{
3333
"path": "name",
34-
// "code": "length",
34+
"code": "length",
3535
"subcode": "min",
3636
"min": 1,
3737
"length": 0
3838
},
3939
{
4040
"path": "emailAddresses",
41-
// "code": "nested",
41+
"code": "nested",
4242
"errors": [
4343
{
4444
"index": 0,
4545
"path": "emailAddress",
46-
// "code": "emailAddress",
46+
"code": "emailAddress",
4747
"subcode": "missingSeparator"
4848
}
4949
]

packages/fortifier-macros/tests/validations/length/options_pass.rs

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use fortifier::{LengthError, Validate, ValidationErrors};
1+
use fortifier::{LengthError, LengthErrorCode, Validate, ValidationErrors};
22

33
#[derive(Validate)]
44
struct LengthData<'a> {
@@ -24,12 +24,25 @@ fn main() {
2424
data.validate_sync(),
2525
Err(ValidationErrors::from_iter([
2626
LengthDataValidationError::Equal(LengthError::Equal {
27+
code: LengthErrorCode,
2728
equal: 2,
2829
length: 1
2930
}),
30-
LengthDataValidationError::Min(LengthError::Min { min: 1, length: 0 }),
31-
LengthDataValidationError::Max(LengthError::Max { max: 4, length: 5 }),
32-
LengthDataValidationError::MinMax(LengthError::Max { max: 4, length: 6 })
31+
LengthDataValidationError::Min(LengthError::Min {
32+
code: LengthErrorCode,
33+
min: 1,
34+
length: 0
35+
}),
36+
LengthDataValidationError::Max(LengthError::Max {
37+
code: LengthErrorCode,
38+
max: 4,
39+
length: 5
40+
}),
41+
LengthDataValidationError::MinMax(LengthError::Max {
42+
code: LengthErrorCode,
43+
max: 4,
44+
length: 6
45+
})
3346
]))
3447
);
3548
}

packages/fortifier-macros/tests/validations/length/types_pass.rs

Lines changed: 66 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet, LinkedList, VecDeque};
22

3-
use fortifier::{LengthError, Validate, ValidationErrors};
3+
use fortifier::{LengthError, LengthErrorCode, Validate, ValidationErrors};
44
use indexmap::{IndexMap, IndexSet};
55

66
#[derive(Validate)]
@@ -53,19 +53,71 @@ fn main() {
5353
assert_eq!(
5454
data.validate_sync(),
5555
Err(ValidationErrors::from_iter([
56-
LengthDataValidationError::Str(LengthError::Min { min: 1, length: 0 }),
57-
LengthDataValidationError::String(LengthError::Min { min: 1, length: 0 }),
58-
LengthDataValidationError::Array(LengthError::Min { min: 1, length: 0 }),
59-
LengthDataValidationError::Slice(LengthError::Min { min: 1, length: 0 }),
60-
LengthDataValidationError::BTreeMap(LengthError::Min { min: 1, length: 0 }),
61-
LengthDataValidationError::BTreeSet(LengthError::Min { min: 1, length: 0 }),
62-
LengthDataValidationError::HashMap(LengthError::Min { min: 1, length: 0 }),
63-
LengthDataValidationError::HashSet(LengthError::Min { min: 1, length: 0 }),
64-
LengthDataValidationError::IndexMap(LengthError::Min { min: 1, length: 0 }),
65-
LengthDataValidationError::IndexSet(LengthError::Min { min: 1, length: 0 }),
66-
LengthDataValidationError::LinkedList(LengthError::Min { min: 1, length: 0 }),
67-
LengthDataValidationError::Vec(LengthError::Min { min: 1, length: 0 }),
68-
LengthDataValidationError::VecDeque(LengthError::Min { min: 1, length: 0 }),
56+
LengthDataValidationError::Str(LengthError::Min {
57+
code: LengthErrorCode,
58+
min: 1,
59+
length: 0
60+
}),
61+
LengthDataValidationError::String(LengthError::Min {
62+
code: LengthErrorCode,
63+
min: 1,
64+
length: 0
65+
}),
66+
LengthDataValidationError::Array(LengthError::Min {
67+
code: LengthErrorCode,
68+
min: 1,
69+
length: 0
70+
}),
71+
LengthDataValidationError::Slice(LengthError::Min {
72+
code: LengthErrorCode,
73+
min: 1,
74+
length: 0
75+
}),
76+
LengthDataValidationError::BTreeMap(LengthError::Min {
77+
code: LengthErrorCode,
78+
min: 1,
79+
length: 0
80+
}),
81+
LengthDataValidationError::BTreeSet(LengthError::Min {
82+
code: LengthErrorCode,
83+
min: 1,
84+
length: 0
85+
}),
86+
LengthDataValidationError::HashMap(LengthError::Min {
87+
code: LengthErrorCode,
88+
min: 1,
89+
length: 0
90+
}),
91+
LengthDataValidationError::HashSet(LengthError::Min {
92+
code: LengthErrorCode,
93+
min: 1,
94+
length: 0
95+
}),
96+
LengthDataValidationError::IndexMap(LengthError::Min {
97+
code: LengthErrorCode,
98+
min: 1,
99+
length: 0
100+
}),
101+
LengthDataValidationError::IndexSet(LengthError::Min {
102+
code: LengthErrorCode,
103+
min: 1,
104+
length: 0
105+
}),
106+
LengthDataValidationError::LinkedList(LengthError::Min {
107+
code: LengthErrorCode,
108+
min: 1,
109+
length: 0
110+
}),
111+
LengthDataValidationError::Vec(LengthError::Min {
112+
code: LengthErrorCode,
113+
min: 1,
114+
length: 0
115+
}),
116+
LengthDataValidationError::VecDeque(LengthError::Min {
117+
code: LengthErrorCode,
118+
min: 1,
119+
length: 0
120+
}),
69121
]))
70122
);
71123
}

packages/fortifier-macros/tests/validations/phone-number/options_pass.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
use fortifier::{PhoneNumberCountry, PhoneNumberError, Validate, ValidationErrors};
1+
use fortifier::{
2+
PhoneNumberCountry, PhoneNumberError, PhoneNumberErrorCode, Validate, ValidationErrors,
3+
};
24
use phonenumber::ParseError;
35

46
#[derive(Validate)]
@@ -29,6 +31,7 @@ fn main() {
2931
)),
3032
PhoneNumberDataValidationError::AllowedCountries(
3133
PhoneNumberError::DisallowedCountryCode {
34+
code: PhoneNumberErrorCode,
3235
allowed: vec![PhoneNumberCountry::GB],
3336
value: Some(PhoneNumberCountry::NL)
3437
}

packages/fortifier-macros/tests/validations/phone-number/types_pass.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
use std::str::FromStr;
22

3-
use fortifier::{PhoneNumberCountry, PhoneNumberError, Validate, ValidationErrors};
3+
use fortifier::{
4+
PhoneNumberCountry, PhoneNumberError, PhoneNumberErrorCode, Validate, ValidationErrors,
5+
};
46
use phonenumber::{ParseError, PhoneNumber};
57

68
#[derive(Validate)]
@@ -28,6 +30,7 @@ fn main() {
2830
)),
2931
PhoneNumberDataValidationError::String(PhoneNumberError::from(ParseError::TooShortNsn)),
3032
PhoneNumberDataValidationError::PhoneNumber(PhoneNumberError::DisallowedCountryCode {
33+
code: PhoneNumberErrorCode,
3134
allowed: vec![PhoneNumberCountry::NL],
3235
value: Some(PhoneNumberCountry::GB),
3336
})
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
/// Implement an error code.
2+
#[macro_export]
3+
macro_rules! error_code {
4+
($name:ident, $code:literal) => {
5+
const CODE: &str = $code;
6+
7+
/// Email address error code.
8+
#[derive(Eq, PartialEq)]
9+
pub struct $name;
10+
11+
impl Default for $name {
12+
fn default() -> Self {
13+
Self
14+
}
15+
}
16+
17+
impl ::std::ops::Deref for $name {
18+
type Target = str;
19+
20+
fn deref(&self) -> &Self::Target {
21+
CODE
22+
}
23+
}
24+
25+
impl ::std::fmt::Debug for $name {
26+
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
27+
::std::fmt::Debug::fmt(&**self, f)
28+
}
29+
}
30+
31+
#[cfg(feature = "serde")]
32+
impl<'de> ::serde::Deserialize<'de> for $name {
33+
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
34+
where
35+
D: ::serde::Deserializer<'de>,
36+
{
37+
deserializer
38+
.deserialize_any($crate::integrations::serde::MustBeStrVisitor(CODE))
39+
.map(|()| Self)
40+
}
41+
}
42+
43+
#[cfg(feature = "serde")]
44+
impl ::serde::Serialize for $name {
45+
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
46+
where
47+
S: ::serde::Serializer,
48+
{
49+
serializer.serialize_str(CODE)
50+
}
51+
}
52+
53+
#[cfg(feature = "utoipa")]
54+
impl ::utoipa::PartialSchema for $name {
55+
fn schema() -> ::utoipa::openapi::RefOr<::utoipa::openapi::schema::Schema> {
56+
::utoipa::openapi::schema::ObjectBuilder::new()
57+
.schema_type(::utoipa::openapi::schema::Type::String)
58+
.enum_values(Some([CODE]))
59+
.build()
60+
.into()
61+
}
62+
}
63+
64+
#[cfg(feature = "utoipa")]
65+
impl ::utoipa::ToSchema for $name {}
66+
};
67+
}

packages/fortifier/src/integrations/serde.rs

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
//! Serde utilities
22
3+
use std::fmt;
4+
5+
use serde::de::{Error, Unexpected, Visitor};
6+
37
/// Deserialize and serialize with `errors` field.
48
pub mod errors {
59
use serde::{Deserialize, Deserializer, Serialize, Serializer};
@@ -27,9 +31,38 @@ pub mod errors {
2731
{
2832
#[derive(Serialize)]
2933
struct Wrapper<'a, T> {
34+
code: &'static str,
3035
errors: &'a T,
3136
}
3237

33-
Wrapper { errors: value }.serialize(serializer)
38+
Wrapper {
39+
code: "nested",
40+
errors: value,
41+
}
42+
.serialize(serializer)
43+
}
44+
}
45+
46+
/// Serde visitor for a static string.
47+
///
48+
/// Based on `MustBeStrVisitor` from [`monostate`](https://crates.io/crates/monostate).
49+
pub struct MustBeStrVisitor(pub &'static str);
50+
51+
impl<'de> Visitor<'de> for MustBeStrVisitor {
52+
type Value = ();
53+
54+
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
55+
write!(formatter, "string {:?}", self.0)
56+
}
57+
58+
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
59+
where
60+
E: Error,
61+
{
62+
if v == self.0 {
63+
Ok(())
64+
} else {
65+
Err(E::invalid_value(Unexpected::Str(v), &self))
66+
}
3467
}
3568
}

packages/fortifier/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
//! Fortifier.
44
55
mod error;
6+
mod error_code;
67
mod integrations;
78
mod validate;
89
mod validations;

0 commit comments

Comments
 (0)