Skip to content

Commit 40a1b1e

Browse files
feat: support unnamed and unit structs (#4)
1 parent 7aced3d commit 40a1b1e

7 files changed

Lines changed: 137 additions & 27 deletions

File tree

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

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
use proc_macro2::TokenStream;
22
use quote::quote;
3-
use syn::{Field, Ident, Result};
3+
use syn::{Field, Result};
44

55
use crate::validations::{Email, Length};
66

77
pub struct ValidateField {
8-
ident: Ident,
8+
expr: TokenStream,
99
// TODO: Consider using a trait for validations.
1010
email: Option<Email>,
1111
length: Option<Length>,
1212
}
1313

1414
impl ValidateField {
15-
pub fn parse(ident: Ident, field: &Field) -> Result<Self> {
15+
pub fn parse(expr: TokenStream, field: &Field) -> Result<Self> {
1616
let mut result = Self {
17-
ident,
17+
expr,
1818
email: None,
1919
length: None,
2020
};
@@ -53,11 +53,8 @@ impl ValidateField {
5353
}
5454

5555
pub fn sync_validations(&self) -> Vec<TokenStream> {
56-
let email = self.email.as_ref().map(|email| email.tokens(&self.ident));
57-
let length = self
58-
.length
59-
.as_ref()
60-
.map(|length| length.tokens(&self.ident));
56+
let email = self.email.as_ref().map(|email| email.tokens(&self.expr));
57+
let length = self.length.as_ref().map(|length| length.tokens(&self.expr));
6158

6259
[email, length].into_iter().flatten().collect()
6360
}

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

Lines changed: 97 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::collections::HashMap;
22

33
use convert_case::{Case, Casing};
4-
use proc_macro2::TokenStream;
4+
use proc_macro2::{Literal, TokenStream};
55
use quote::{ToTokens, TokenStreamExt, format_ident, quote};
66
use syn::{DataStruct, DeriveInput, Fields, FieldsNamed, FieldsUnnamed, Ident, Result};
77

@@ -54,10 +54,11 @@ impl ValidateNamedStruct {
5454
continue;
5555
};
5656

57-
result.fields.insert(
58-
field_ident.clone(),
59-
ValidateField::parse(field_ident.clone(), field)?,
60-
);
57+
let expr = quote!(self.#field_ident);
58+
59+
result
60+
.fields
61+
.insert(field_ident.clone(), ValidateField::parse(expr, field)?);
6162
}
6263

6364
Ok(result)
@@ -147,26 +148,108 @@ impl ToTokens for ValidateNamedStruct {
147148
}
148149

149150
pub struct ValidateUnnamedStruct {
150-
#[allow(unused)]
151151
ident: Ident,
152-
#[allow(unused)]
152+
error_ident: Ident,
153153
fields: Vec<ValidateField>,
154154
}
155155

156156
impl ValidateUnnamedStruct {
157-
fn parse(input: &DeriveInput, _data: &DataStruct, _fields: &FieldsUnnamed) -> Result<Self> {
158-
let result = Self {
157+
fn parse(input: &DeriveInput, _data: &DataStruct, fields: &FieldsUnnamed) -> Result<Self> {
158+
let mut result = Self {
159159
ident: input.ident.clone(),
160+
error_ident: format_ident!("{}ValidationError", input.ident),
160161
fields: Vec::default(),
161162
};
162163

164+
for (index, field) in fields.unnamed.iter().enumerate() {
165+
let index = Literal::usize_unsuffixed(index);
166+
let expr = quote!(self.#index);
167+
168+
result.fields.push(ValidateField::parse(expr, field)?);
169+
}
170+
163171
Ok(result)
164172
}
165173
}
166174

167175
impl ToTokens for ValidateUnnamedStruct {
168-
fn to_tokens(&self, _tokens: &mut TokenStream) {
169-
// TODO
176+
fn to_tokens(&self, tokens: &mut TokenStream) {
177+
let ident = &self.ident;
178+
let error_ident = &self.error_ident;
179+
let mut error_field_idents = vec![];
180+
let mut error_field_types = vec![];
181+
let mut sync_validations = vec![];
182+
let mut async_validations = vec![];
183+
184+
for (index, field) in self.fields.iter().enumerate() {
185+
let field_error_ident = format_ident!("F{index}");
186+
187+
error_field_idents.push(field_error_ident.clone());
188+
error_field_types.push(field.error_type());
189+
190+
for validation in field.sync_validations() {
191+
sync_validations.push(quote! {
192+
if let Err(err) = #validation {
193+
errors.push(#error_ident::#field_error_ident(err));
194+
}
195+
});
196+
}
197+
198+
for validation in field.async_validations() {
199+
async_validations.push(quote! {
200+
if let Err(err) = #validation {
201+
errors.push(#error_ident::#field_error_ident(err));
202+
}
203+
});
204+
}
205+
}
206+
207+
tokens.append_all(quote! {
208+
use fortifier::*;
209+
210+
#[derive(Debug)]
211+
enum #error_ident {
212+
#( #error_field_idents(#error_field_types) ),*
213+
}
214+
215+
impl ::std::fmt::Display for #error_ident {
216+
fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result {
217+
write!(f, "{self:#?}")
218+
}
219+
}
220+
221+
impl ::std::error::Error for #error_ident {}
222+
223+
impl Validate for #ident {
224+
type Error = #error_ident;
225+
226+
fn validate_sync(&self) -> Result<(), ValidationErrors<Self::Error>> {
227+
let mut errors = vec![];
228+
229+
#(#sync_validations)*
230+
231+
if !errors.is_empty() {
232+
Err(errors.into())
233+
} else {
234+
Ok(())
235+
}
236+
}
237+
238+
fn validate_async(&self) -> ::std::pin::Pin<Box<impl Future<Output = Result<(), ValidationErrors<Self::Error>>>>> {
239+
Box::pin(async {
240+
let mut errors = vec![];
241+
242+
#(#async_validations)*
243+
244+
if !errors.is_empty() {
245+
Err(errors.into())
246+
} else {
247+
Ok(())
248+
}
249+
})
250+
}
251+
}
252+
})
170253
}
171254
}
172255

@@ -187,8 +270,10 @@ impl ToTokens for ValidateUnitStruct {
187270
let ident = &self.ident;
188271

189272
tokens.append_all(quote! {
273+
use fortifier::ValidationErrors;
274+
190275
impl Validate for #ident {
191-
type Error = Infallible;
276+
type Error = ::std::convert::Infallible;
192277

193278
fn validate_sync(&self) -> Result<(), ValidationErrors<Self::Error>> {
194279
Ok(())
Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use proc_macro2::TokenStream;
22
use quote::quote;
3-
use syn::{Ident, Result, meta::ParseNestedMeta};
3+
use syn::{Result, meta::ParseNestedMeta};
44

55
#[derive(Default)]
66
pub struct Email {}
@@ -10,9 +10,9 @@ impl Email {
1010
Ok(Email::default())
1111
}
1212

13-
pub fn tokens(&self, ident: &Ident) -> TokenStream {
13+
pub fn tokens(&self, expr: &TokenStream) -> TokenStream {
1414
quote! {
15-
self.#ident.validate_email()
15+
#expr.validate_email()
1616
}
1717
}
1818
}

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

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

55
#[derive(Default)]
66
pub struct Length {
@@ -37,7 +37,7 @@ impl Length {
3737
Ok(result)
3838
}
3939

40-
pub fn tokens(&self, ident: &Ident) -> TokenStream {
40+
pub fn tokens(&self, expr: &TokenStream) -> TokenStream {
4141
let equal = if let Some(equal) = &self.equal {
4242
quote!(Some(#equal))
4343
} else {
@@ -55,7 +55,7 @@ impl Length {
5555
};
5656

5757
quote! {
58-
self.#ident.validate_length(#equal, #min, #max)
58+
#expr.validate_length(#equal, #min, #max)
5959
}
6060
}
6161
}

packages/fortifier-macros/tests/derive/basic_pass.rs renamed to packages/fortifier-macros/tests/derive/struct_named_pass.rs

File renamed without changes.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use std::error::Error;
2+
3+
use fortifier::Validate;
4+
5+
#[derive(Validate)]
6+
struct CreateUser;
7+
8+
fn main() -> Result<(), Box<dyn Error>> {
9+
let data = CreateUser;
10+
11+
data.validate_sync()?;
12+
13+
Ok(())
14+
}
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
use std::error::Error;
2+
3+
use fortifier::Validate;
4+
5+
#[derive(Validate)]
6+
struct CreateUser(#[validate(length(min = 1, max = 256))] String);
7+
8+
fn main() -> Result<(), Box<dyn Error>> {
9+
let data = CreateUser("John Doe".to_owned());
10+
11+
data.validate_sync()?;
12+
13+
Ok(())
14+
}

0 commit comments

Comments
 (0)