Skip to content

Commit d34f3c6

Browse files
feat: merge field errors
1 parent 59f2038 commit d34f3c6

8 files changed

Lines changed: 135 additions & 26 deletions

File tree

example/src/main.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ struct CreateUser {
1414
url: String,
1515

1616
#[validate(custom(function = validate_one_locale_required, error = OneLocaleRequiredError))]
17+
#[validate(length(min = 1))]
1718
locales: Vec<String>,
1819
}
1920

packages/fortifier-macros/src/validate/field.rs

Lines changed: 68 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,31 @@
1+
use convert_case::{Case, Casing};
12
use proc_macro2::TokenStream;
2-
use quote::quote;
3-
use syn::{Field, Result};
3+
use quote::{ToTokens, format_ident, quote};
4+
use syn::{Field, Ident, Result};
45

56
use crate::{
67
validation::Validation,
78
validations::{Custom, Email, Length, Url},
89
};
910

1011
pub struct ValidateField {
12+
error_type_ident: Ident,
1113
expr: TokenStream,
1214
validations: Vec<Box<dyn Validation>>,
1315
}
1416

1517
impl ValidateField {
16-
pub fn parse(expr: TokenStream, field: &Field) -> Result<Self> {
18+
pub fn parse(
19+
type_prefix: &Ident,
20+
ident: Ident,
21+
expr: TokenStream,
22+
field: &Field,
23+
) -> Result<Self> {
24+
let error_ident = format_ident!("{}", ident.to_string().to_case(Case::UpperCamel));
25+
let error_type_ident = format_ident!("{type_prefix}{error_ident}ValidationError");
26+
1727
let mut result = Self {
28+
error_type_ident,
1829
expr,
1930
validations: vec![],
2031
};
@@ -48,28 +59,74 @@ impl ValidateField {
4859
Ok(result)
4960
}
5061

51-
pub fn error_type(&self) -> TokenStream {
52-
// TODO: Merge error types
62+
pub fn error_type(
63+
&self,
64+
ident: &Ident,
65+
field_error_ident: &Ident,
66+
) -> (TokenStream, Option<TokenStream>) {
67+
if self.validations.len() > 1 {
68+
let ident = format_ident!("{}{}ValidationError", ident, field_error_ident);
69+
let variant_ident = self.validations.iter().map(|validation| validation.ident());
70+
let variant_type = self
71+
.validations
72+
.iter()
73+
.map(|validation| validation.error_type());
5374

54-
self.validations
55-
.first()
56-
.map(|validation| validation.error_type())
57-
.unwrap_or_else(|| quote!(()))
75+
(
76+
ident.to_token_stream(),
77+
Some(quote! {
78+
#[derive(Debug)]
79+
enum #ident {
80+
#( #variant_ident(#variant_type) ),*
81+
}
82+
}),
83+
)
84+
} else if let Some(validation) = self.validations.first() {
85+
(validation.error_type(), None)
86+
} else {
87+
(quote!(()), None)
88+
}
5889
}
5990

6091
pub fn sync_validations(&self) -> Vec<TokenStream> {
92+
let error_type_ident = &self.error_type_ident;
93+
6194
self.validations
6295
.iter()
6396
.filter(|validation| !validation.is_async())
64-
.map(|validation| validation.tokens(&self.expr))
97+
.map(|validation| {
98+
let validation_ident = validation.ident();
99+
let tokens = validation.tokens(&self.expr);
100+
101+
if self.validations.len() > 1 {
102+
quote! {
103+
#tokens.map_err(#error_type_ident::#validation_ident)
104+
}
105+
} else {
106+
tokens
107+
}
108+
})
65109
.collect()
66110
}
67111

68112
pub fn async_validations(&self) -> Vec<TokenStream> {
113+
let error_type_ident = &self.error_type_ident;
114+
69115
self.validations
70116
.iter()
71117
.filter(|validation| validation.is_async())
72-
.map(|validation| validation.tokens(&self.expr))
118+
.map(|validation| {
119+
let validation_ident = validation.ident();
120+
let tokens = validation.tokens(&self.expr);
121+
122+
if self.validations.len() > 1 {
123+
quote! {
124+
#tokens.map_err(#error_type_ident::#validation_ident)
125+
}
126+
} else {
127+
tokens
128+
}
129+
})
73130
.collect()
74131
}
75132
}

packages/fortifier-macros/src/validate/struct.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -56,9 +56,10 @@ impl ValidateNamedStruct {
5656

5757
let expr = quote!(self.#field_ident);
5858

59-
result
60-
.fields
61-
.insert(field_ident.clone(), ValidateField::parse(expr, field)?);
59+
result.fields.insert(
60+
field_ident.clone(),
61+
ValidateField::parse(&input.ident, field_ident.clone(), expr, field)?,
62+
);
6263
}
6364

6465
Ok(result)
@@ -71,15 +72,21 @@ impl ToTokens for ValidateNamedStruct {
7172
let error_ident = &self.error_ident;
7273
let mut error_field_idents = vec![];
7374
let mut error_field_types = vec![];
75+
let mut error_field_enums = vec![];
7476
let mut sync_validations = vec![];
7577
let mut async_validations = vec![];
7678

7779
for (field_ident, field) in &self.fields {
7880
let field_error_ident =
7981
format_ident!("{}", &field_ident.to_string().to_case(Case::UpperCamel));
8082

83+
let (error_type, error_enum) = field.error_type(ident, &field_error_ident);
84+
8185
error_field_idents.push(field_error_ident.clone());
82-
error_field_types.push(field.error_type());
86+
error_field_types.push(error_type);
87+
if let Some(error_enum) = error_enum {
88+
error_field_enums.push(error_enum);
89+
}
8390

8491
for validation in field.sync_validations() {
8592
sync_validations.push(quote! {
@@ -101,19 +108,25 @@ impl ToTokens for ValidateNamedStruct {
101108
tokens.append_all(quote! {
102109
use fortifier::*;
103110

111+
#[allow(dead_code)]
104112
#[derive(Debug)]
105113
enum #error_ident {
106114
#( #error_field_idents(#error_field_types) ),*
107115
}
108116

117+
#[automatically_derived]
109118
impl ::std::fmt::Display for #error_ident {
110119
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
111120
write!(f, "{self:#?}")
112121
}
113122
}
114123

124+
#[automatically_derived]
115125
impl ::std::error::Error for #error_ident {}
116126

127+
#(#error_field_enums)*
128+
129+
#[automatically_derived]
117130
impl Validate for #ident {
118131
type Error = #error_ident;
119132

@@ -163,9 +176,15 @@ impl ValidateUnnamedStruct {
163176

164177
for (index, field) in fields.unnamed.iter().enumerate() {
165178
let index = Literal::usize_unsuffixed(index);
179+
let field_ident = format_ident!("F{index}");
166180
let expr = quote!(self.#index);
167181

168-
result.fields.push(ValidateField::parse(expr, field)?);
182+
result.fields.push(ValidateField::parse(
183+
&input.ident,
184+
field_ident,
185+
expr,
186+
field,
187+
)?);
169188
}
170189

171190
Ok(result)
@@ -178,14 +197,20 @@ impl ToTokens for ValidateUnnamedStruct {
178197
let error_ident = &self.error_ident;
179198
let mut error_field_idents = vec![];
180199
let mut error_field_types = vec![];
200+
let mut error_field_enums = vec![];
181201
let mut sync_validations = vec![];
182202
let mut async_validations = vec![];
183203

184204
for (index, field) in self.fields.iter().enumerate() {
185205
let field_error_ident = format_ident!("F{index}");
186206

207+
let (error_type, error_enum) = field.error_type(ident, &field_error_ident);
208+
187209
error_field_idents.push(field_error_ident.clone());
188-
error_field_types.push(field.error_type());
210+
error_field_types.push(error_type);
211+
if let Some(error_enum) = error_enum {
212+
error_field_enums.push(error_enum);
213+
}
189214

190215
for validation in field.sync_validations() {
191216
sync_validations.push(quote! {
@@ -207,19 +232,25 @@ impl ToTokens for ValidateUnnamedStruct {
207232
tokens.append_all(quote! {
208233
use fortifier::*;
209234

235+
#[allow(dead_code)]
210236
#[derive(Debug)]
211237
enum #error_ident {
212238
#( #error_field_idents(#error_field_types) ),*
213239
}
214240

241+
#[automatically_derived]
215242
impl ::std::fmt::Display for #error_ident {
216243
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
217244
write!(f, "{self:#?}")
218245
}
219246
}
220247

248+
#[automatically_derived]
221249
impl ::std::error::Error for #error_ident {}
222250

251+
#(#error_field_enums)*
252+
253+
#[automatically_derived]
223254
impl Validate for #ident {
224255
type Error = #error_ident;
225256

@@ -272,6 +303,7 @@ impl ToTokens for ValidateUnitStruct {
272303
tokens.append_all(quote! {
273304
use fortifier::ValidationErrors;
274305

306+
#[automatically_derived]
275307
impl Validate for #ident {
276308
type Error = ::std::convert::Infallible;
277309

packages/fortifier-macros/src/validation.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
use proc_macro2::TokenStream;
2-
use syn::{Result, meta::ParseNestedMeta};
2+
use syn::{Ident, Result, meta::ParseNestedMeta};
33

44
pub trait Validation {
55
fn parse(_meta: &ParseNestedMeta<'_>) -> Result<Self>
@@ -8,6 +8,8 @@ pub trait Validation {
88

99
fn is_async(&self) -> bool;
1010

11+
fn ident(&self) -> Ident;
12+
1113
fn error_type(&self) -> TokenStream;
1214

1315
fn tokens(&self, expr: &TokenStream) -> TokenStream;

packages/fortifier-macros/src/validations/custom.rs

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use proc_macro2::TokenStream;
2-
use quote::{ToTokens, quote};
3-
use syn::{LitBool, Path, Result, Type, meta::ParseNestedMeta};
2+
use quote::{ToTokens, format_ident, quote};
3+
use syn::{Ident, LitBool, Path, Result, Type, meta::ParseNestedMeta};
44

55
use crate::validation::Validation;
66

@@ -57,6 +57,11 @@ impl Validation for Custom {
5757
self.is_async
5858
}
5959

60+
fn ident(&self) -> Ident {
61+
// TODO: Determine ident from function or error type.
62+
format_ident!("Custom")
63+
}
64+
6065
fn error_type(&self) -> TokenStream {
6166
self.error_type.to_token_stream()
6267
}

packages/fortifier-macros/src/validations/email.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use proc_macro2::TokenStream;
2-
use quote::quote;
3-
use syn::{Result, meta::ParseNestedMeta};
2+
use quote::{format_ident, quote};
3+
use syn::{Ident, Result, meta::ParseNestedMeta};
44

55
use crate::validation::Validation;
66

@@ -16,6 +16,10 @@ impl Validation for Email {
1616
false
1717
}
1818

19+
fn ident(&self) -> Ident {
20+
format_ident!("Email")
21+
}
22+
1923
fn error_type(&self) -> TokenStream {
2024
quote!(EmailError)
2125
}

packages/fortifier-macros/src/validations/length.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use proc_macro2::TokenStream;
2-
use quote::quote;
3-
use syn::{Expr, Result, meta::ParseNestedMeta};
2+
use quote::{format_ident, quote};
3+
use syn::{Expr, Ident, Result, meta::ParseNestedMeta};
44

55
use crate::validation::Validation;
66

@@ -43,6 +43,10 @@ impl Validation for Length {
4343
false
4444
}
4545

46+
fn ident(&self) -> Ident {
47+
format_ident!("Length")
48+
}
49+
4650
fn error_type(&self) -> TokenStream {
4751
quote!(LengthError<usize>)
4852
}

packages/fortifier-macros/src/validations/url.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use proc_macro2::TokenStream;
2-
use quote::quote;
3-
use syn::{Result, meta::ParseNestedMeta};
2+
use quote::{format_ident, quote};
3+
use syn::{Ident, Result, meta::ParseNestedMeta};
44

55
use crate::validation::Validation;
66

@@ -16,6 +16,10 @@ impl Validation for Url {
1616
false
1717
}
1818

19+
fn ident(&self) -> Ident {
20+
format_ident!("Url")
21+
}
22+
1923
fn error_type(&self) -> TokenStream {
2024
quote!(UrlError)
2125
}

0 commit comments

Comments
 (0)