Skip to content

Commit fe21c38

Browse files
feat: nested validation detection
1 parent 16b74e2 commit fe21c38

14 files changed

Lines changed: 224 additions & 41 deletions

packages/fortifier-macros/src/validate.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod r#enum;
33
mod field;
44
mod fields;
55
mod r#struct;
6+
mod r#type;
67
mod r#union;
78

89
use proc_macro2::TokenStream;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use proc_macro2::TokenStream;
22
use quote::quote;
33

44
pub fn enum_attributes() -> TokenStream {
5+
#[allow(unused_mut)]
56
let mut attributes: Vec<TokenStream> = vec![];
67

78
#[cfg(feature = "serde")]
@@ -12,7 +13,6 @@ pub fn enum_attributes() -> TokenStream {
1213
attributes.push(quote! {
1314
#[derive(serde::Deserialize, serde::Serialize)]
1415
#[serde(
15-
// TODO: Tag?
1616
tag = "path",
1717
rename_all = "camelCase",
1818
rename_all_fields = "camelCase"

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use quote::{ToTokens, format_ident, quote};
44
use syn::{Field, Ident, Result, Visibility};
55

66
use crate::{
7-
validate::attributes::enum_attributes,
7+
validate::{attributes::enum_attributes, r#type::should_validate_type},
88
validation::{Execution, Validation},
99
validations::{Custom, Email, Length, Regex, Url},
1010
};
@@ -60,6 +60,7 @@ impl<'a> ValidateField<'a> {
6060
error_type_ident,
6161
validations: vec![],
6262
};
63+
let mut skip = false;
6364

6465
for attr in &field.attrs {
6566
if attr.path().is_ident("validate") {
@@ -83,6 +84,10 @@ impl<'a> ValidateField<'a> {
8384
} else if meta.path.is_ident("url") {
8485
result.validations.push(Box::new(Url::parse(&meta)?));
8586

87+
Ok(())
88+
} else if meta.path.is_ident("skip") {
89+
skip = true;
90+
8691
Ok(())
8792
} else {
8893
Err(meta.error("unknown parameter"))
@@ -91,6 +96,10 @@ impl<'a> ValidateField<'a> {
9196
}
9297
}
9398

99+
if !skip && should_validate_type(&field.ty) {
100+
// TODO: Nested validation
101+
}
102+
94103
Ok(result)
95104
}
96105

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
use syn::{GenericArgument, Path, PathArguments, Type, TypeParamBound};
2+
3+
const PRIMITIVE_AND_BUILT_IN_TYPES: [&str; 18] = [
4+
"bool", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
5+
"f32", "f64", "char", "str", "String",
6+
];
7+
8+
const CONTAINER_TYPES: [&str; 20] = [
9+
"Arc",
10+
"BTreeMap",
11+
"BTreeSet",
12+
"HashMap",
13+
"HashSet",
14+
"LinkedList",
15+
"Option",
16+
"Rc",
17+
"Vec",
18+
"VecDeque",
19+
"std::collections::BTreeMap",
20+
"std::collections::BTreeSet",
21+
"std::collections::HashMap",
22+
"std::collections::HashSet",
23+
"std::collections::LinkedList",
24+
"std::collections::VecDeque",
25+
"std::option::Option",
26+
"std::rc::Rc",
27+
"std::sync::Arc",
28+
"std::vec::Vec",
29+
];
30+
31+
fn path_to_string(path: &Path) -> String {
32+
// TODO: This is probably slow, replace with comparisons.
33+
path.segments
34+
.iter()
35+
.map(|segment| segment.ident.to_string())
36+
.collect::<Vec<_>>()
37+
.join("::")
38+
}
39+
40+
fn is_validate_path(path: &Path) -> bool {
41+
let path_string = path_to_string(path);
42+
path_string == "Validate" || path_string == "fortifier::Validate"
43+
}
44+
45+
fn should_validate_generic_argument(arg: &GenericArgument) -> bool {
46+
match arg {
47+
GenericArgument::Lifetime(_) => true,
48+
GenericArgument::Type(r#type) => should_validate_type(r#type),
49+
GenericArgument::Const(_expr) => todo!(),
50+
GenericArgument::AssocType(_assoc_type) => todo!(),
51+
GenericArgument::AssocConst(_assoc_const) => todo!(),
52+
GenericArgument::Constraint(_constraint) => todo!(),
53+
_ => true,
54+
}
55+
}
56+
57+
fn should_validate_path(path: &Path) -> bool {
58+
if let Some(ident) = path.get_ident() {
59+
return !PRIMITIVE_AND_BUILT_IN_TYPES.contains(&ident.to_string().as_str());
60+
}
61+
let path_string = path_to_string(path);
62+
63+
if CONTAINER_TYPES.contains(&path_string.as_str())
64+
&& let Some(segment) = path.segments.last()
65+
&& let PathArguments::AngleBracketed(arguments) = &segment.arguments
66+
&& !arguments.args.iter().all(should_validate_generic_argument)
67+
{
68+
return false;
69+
}
70+
71+
true
72+
}
73+
74+
pub fn should_validate_type(r#type: &Type) -> bool {
75+
match r#type {
76+
Type::Array(r#type) => should_validate_type(&r#type.elem),
77+
Type::BareFn(_) => false,
78+
Type::Group(r#type) => should_validate_type(&r#type.elem),
79+
Type::ImplTrait(r#type) => r#type.bounds.iter().any(
80+
|bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path)),
81+
),
82+
Type::Infer(_) => true,
83+
Type::Macro(_) => true,
84+
Type::Never(_) => false,
85+
Type::Paren(r#type) => should_validate_type(&r#type.elem),
86+
Type::Path(r#type) => should_validate_path(&r#type.path),
87+
Type::Ptr(r#type) => should_validate_type(&r#type.elem),
88+
Type::Reference(r#type) => should_validate_type(&r#type.elem),
89+
Type::Slice(r#type) => should_validate_type(&r#type.elem),
90+
Type::TraitObject(r#type) => r#type.bounds.iter().any(
91+
|bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path)),
92+
),
93+
Type::Tuple(r#type) => {
94+
!r#type.elems.is_empty() && r#type.elems.iter().all(should_validate_type)
95+
}
96+
Type::Verbatim(_) => false,
97+
_ => false,
98+
}
99+
}
100+
101+
#[cfg(test)]
102+
mod tests {
103+
use proc_macro2::TokenStream;
104+
use quote::quote;
105+
106+
use super::should_validate_type;
107+
108+
fn validate(tokens: TokenStream) -> bool {
109+
should_validate_type(&syn::parse2(tokens).expect("Type should be valid."))
110+
}
111+
112+
#[test]
113+
fn should_validate() {
114+
assert!(validate(quote!(&T)));
115+
assert!(validate(quote!(T)));
116+
117+
assert!(validate(quote!((T, T))));
118+
assert!(validate(quote!((A, B, C))));
119+
120+
assert!(validate(quote!([T])));
121+
assert!(validate(quote!([T; 3])));
122+
assert!(validate(quote!([&T])));
123+
assert!(validate(quote!([&T; 3])));
124+
assert!(validate(quote!(&[T])));
125+
assert!(validate(quote!(&[T; 3])));
126+
127+
assert!(validate(quote!(Arc<T>)));
128+
assert!(validate(quote!(BTreeSet<T>)));
129+
assert!(validate(quote!(BTreeMap<K, V>)));
130+
assert!(validate(quote!(HashSet<T>)));
131+
assert!(validate(quote!(HashMap<K, V>)));
132+
assert!(validate(quote!(LinkedList<T>)));
133+
assert!(validate(quote!(Option<T>)));
134+
assert!(validate(quote!(Option<Option<T>>)));
135+
assert!(validate(quote!(Rc<T>)));
136+
assert!(validate(quote!(Vec<T>)));
137+
assert!(validate(quote!(VecDeque<T>)));
138+
139+
assert!(validate(quote!(impl Validate)));
140+
assert!(validate(quote!(impl fortifier::Validate)));
141+
assert!(validate(quote!(dyn Validate)));
142+
assert!(validate(quote!(dyn ::fortifier::Validate)));
143+
}
144+
145+
#[test]
146+
fn should_not_validate() {
147+
assert!(!validate(quote!(bool)));
148+
assert!(!validate(quote!(i8)));
149+
assert!(!validate(quote!(i16)));
150+
assert!(!validate(quote!(i32)));
151+
assert!(!validate(quote!(i64)));
152+
assert!(!validate(quote!(i128)));
153+
assert!(!validate(quote!(isize)));
154+
assert!(!validate(quote!(u8)));
155+
assert!(!validate(quote!(u16)));
156+
assert!(!validate(quote!(u32)));
157+
assert!(!validate(quote!(u64)));
158+
assert!(!validate(quote!(u128)));
159+
assert!(!validate(quote!(usize)));
160+
assert!(!validate(quote!(f32)));
161+
assert!(!validate(quote!(f64)));
162+
assert!(!validate(quote!(char)));
163+
assert!(!validate(quote!(&str)));
164+
assert!(!validate(quote!(String)));
165+
166+
assert!(!validate(quote!(())));
167+
assert!(!validate(quote!((bool, bool))));
168+
assert!(!validate(quote!((usize, usize, usize))));
169+
assert!(!validate(quote!((usize, &str))));
170+
171+
assert!(!validate(quote!([isize])));
172+
assert!(!validate(quote!([&str; 3])));
173+
assert!(!validate(quote!(&[isize])));
174+
assert!(!validate(quote!(&[&str; 3])));
175+
176+
assert!(!validate(quote!(Arc<&str>)));
177+
assert!(!validate(quote!(BTreeSet<usize>)));
178+
assert!(!validate(quote!(BTreeMap<usize, &str>)));
179+
assert!(!validate(quote!(HashSet<&str>)));
180+
assert!(!validate(quote!(HashMap<&str, &str>)));
181+
assert!(!validate(quote!(LinkedList<char>)));
182+
assert!(!validate(quote!(Option<char>)));
183+
assert!(!validate(quote!(Option<Option<String>>)));
184+
assert!(!validate(quote!(Rc<&str>)));
185+
assert!(!validate(quote!(Vec<usize>)));
186+
assert!(!validate(quote!(VecDeque<String>)));
187+
188+
assert!(!validate(quote!(impl Serialize)));
189+
assert!(!validate(quote!(dyn Serialize)));
190+
}
191+
}

packages/fortifier-macros/tests/derive/enum_named_pass.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::error::Error;
2-
3-
use fortifier::Validate;
1+
use fortifier::{Validate, ValidationErrors};
42

53
#[derive(Validate)]
64
enum ChangeEmailAddressRelation {
@@ -19,7 +17,7 @@ enum ChangeEmailAddressRelation {
1917
},
2018
}
2119

22-
fn main() -> Result<(), Box<dyn Error>> {
20+
fn main() -> Result<(), ValidationErrors<ChangeEmailAddressRelationValidationError>> {
2321
let data = ChangeEmailAddressRelation::Create {
2422
email_address: "john@doe.com".to_owned(),
2523
};

packages/fortifier-macros/tests/derive/enum_unit_pass.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::error::Error;
2-
3-
use fortifier::Validate;
1+
use fortifier::{Validate, ValidationErrors};
42

53
#[derive(Validate)]
64
enum ChangeEmailAddressRelation {
@@ -9,7 +7,7 @@ enum ChangeEmailAddressRelation {
97
Delete,
108
}
119

12-
fn main() -> Result<(), Box<dyn Error>> {
10+
fn main() -> Result<(), ValidationErrors<ChangeEmailAddressRelationValidationError>> {
1311
let data = ChangeEmailAddressRelation::Create;
1412

1513
data.validate_sync()?;

packages/fortifier-macros/tests/derive/enum_unnamed_pass.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::error::Error;
2-
3-
use fortifier::Validate;
1+
use fortifier::{Validate, ValidationErrors};
42

53
#[derive(Validate)]
64
enum ChangeEmailAddressRelation {
@@ -9,7 +7,7 @@ enum ChangeEmailAddressRelation {
97
Delete(String),
108
}
119

12-
fn main() -> Result<(), Box<dyn Error>> {
10+
fn main() -> Result<(), ValidationErrors<ChangeEmailAddressRelationValidationError>> {
1311
let data = ChangeEmailAddressRelation::Create("john@doe.com".to_owned());
1412

1513
data.validate_sync()?;

packages/fortifier-macros/tests/derive/struct_named_generics_pass.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::error::Error;
2-
3-
use fortifier::{Validate, ValidateEmail, ValidateLength};
1+
use fortifier::{Validate, ValidateEmail, ValidateLength, ValidationErrors};
42

53
#[derive(Validate)]
64
struct CreateUser<E: ValidateEmail, N: ValidateLength<usize>> {
@@ -11,7 +9,7 @@ struct CreateUser<E: ValidateEmail, N: ValidateLength<usize>> {
119
name: N,
1210
}
1311

14-
fn main() -> Result<(), Box<dyn Error>> {
12+
fn main() -> Result<(), ValidationErrors<CreateUserValidationError>> {
1513
let data = CreateUser {
1614
email: "john@doe.com",
1715
name: "John Doe",

packages/fortifier-macros/tests/derive/struct_named_lifetimes_pass.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::error::Error;
2-
3-
use fortifier::Validate;
1+
use fortifier::{Validate, ValidationErrors};
42

53
#[derive(Validate)]
64
struct CreateUser<'a, 'b> {
@@ -11,7 +9,7 @@ struct CreateUser<'a, 'b> {
119
name: &'b str,
1210
}
1311

14-
fn main() -> Result<(), Box<dyn Error>> {
12+
fn main() -> Result<(), ValidationErrors<CreateUserValidationError>> {
1513
let data = CreateUser {
1614
email: "john@doe.com",
1715
name: "John Doe",

packages/fortifier-macros/tests/derive/struct_named_pass.rs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
use std::error::Error;
2-
3-
use fortifier::Validate;
1+
use fortifier::{Validate, ValidationErrors};
42

53
#[derive(Validate)]
64
struct CreateUser {
@@ -11,7 +9,7 @@ struct CreateUser {
119
name: String,
1210
}
1311

14-
fn main() -> Result<(), Box<dyn Error>> {
12+
fn main() -> Result<(), ValidationErrors<CreateUserValidationError>> {
1513
let data = CreateUser {
1614
email: "john@doe.com".to_owned(),
1715
name: "John Doe".to_owned(),

0 commit comments

Comments
 (0)