Skip to content

Commit e144d3f

Browse files
feat: add error types for nested validation (#33)
1 parent 3da8ff0 commit e144d3f

9 files changed

Lines changed: 379 additions & 79 deletions

File tree

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use syn::{DataEnum, DeriveInput, Ident, Result, Variant, Visibility};
55
use crate::{
66
validate::{
77
attributes::enum_attributes,
8-
field::{LiteralOrIdent, ValidateFieldPrefix},
8+
field::{LiteralOrIdent, ValidateFieldPrefix, format_error_ident},
99
fields::ValidateFields,
1010
},
1111
validation::Execution,
@@ -23,7 +23,7 @@ impl<'a> ValidateEnum<'a> {
2323
let mut result = ValidateEnum {
2424
visibility: &input.vis,
2525
ident: &input.ident,
26-
error_ident: format_ident!("{}ValidationError", input.ident),
26+
error_ident: format_error_ident(&input.ident),
2727
variants: Vec::with_capacity(data.variants.len()),
2828
};
2929

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

Lines changed: 34 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
use convert_case::{Case, Casing};
22
use proc_macro2::{Literal, TokenStream};
33
use quote::{ToTokens, format_ident, quote};
4-
use syn::{Field, Ident, Result, Visibility};
4+
use syn::{Error, Field, Ident, Result, Visibility};
55

66
use crate::{
7-
validate::{attributes::enum_attributes, r#type::should_validate_type},
7+
validate::{
8+
attributes::enum_attributes,
9+
r#type::{KnownOrUnknown, should_validate_type},
10+
},
811
validation::{Execution, Validation},
912
validations::{Custom, EmailAddress, Length, Nested, PhoneNumber, Regex, Url},
1013
};
@@ -49,7 +52,7 @@ impl<'a> ValidateField<'a> {
4952
LiteralOrIdent::Literal(literal) => format_ident!("F{literal}"),
5053
LiteralOrIdent::Ident(ident) => upper_camel_ident(ident),
5154
};
52-
let error_type_ident = format_ident!("{type_prefix}{error_ident}ValidationError");
55+
let error_type_ident = format_error_ident_with_prefix(type_prefix, &error_ident);
5356

5457
let mut result = Self {
5558
visibility,
@@ -58,7 +61,7 @@ impl<'a> ValidateField<'a> {
5861
error_type_ident,
5962
validations: vec![],
6063
};
61-
let mut skip = false;
64+
let mut skip_nested = false;
6265

6366
for attr in &field.attrs {
6467
if attr.path().is_ident("validate") {
@@ -76,6 +79,11 @@ impl<'a> ValidateField<'a> {
7679
} else if meta.path.is_ident("length") {
7780
result.validations.push(Box::new(Length::parse(&meta)?));
7881

82+
Ok(())
83+
} else if meta.path.is_ident("nested") {
84+
result.validations.push(Box::new(Nested::parse(&meta)?));
85+
skip_nested = true;
86+
7987
Ok(())
8088
} else if meta.path.is_ident("phone_number") {
8189
result
@@ -92,7 +100,7 @@ impl<'a> ValidateField<'a> {
92100

93101
Ok(())
94102
} else if meta.path.is_ident("skip") {
95-
skip = true;
103+
skip_nested = true;
96104

97105
Ok(())
98106
} else {
@@ -104,9 +112,18 @@ impl<'a> ValidateField<'a> {
104112

105113
// TODO: Use enum/struct generics to determine if a generic field type supports nested validation.
106114
// TODO: Remove the validations empty check after resolving the issue above.
107-
if !skip && result.validations.is_empty() && should_validate_type(&field.ty) {
108-
// TODO: Nested validation
109-
result.validations.push(Box::new(Nested::new()));
115+
if !skip_nested
116+
&& result.validations.is_empty()
117+
&& let Some(nested_type) = should_validate_type(&field.ty)
118+
{
119+
if let KnownOrUnknown::Known(nested_type) = nested_type {
120+
result.validations.push(Box::new(Nested::new(nested_type)));
121+
} else {
122+
return Err(Error::new_spanned(
123+
field,
124+
"error type must be specified using `#[validate(nested(error_type = MyErrorType))]`",
125+
));
126+
}
110127
}
111128

112129
Ok(result)
@@ -124,7 +141,7 @@ impl<'a> ValidateField<'a> {
124141
if self.validations.len() > 1 {
125142
let attributes = enum_attributes();
126143
let visibility = &self.visibility;
127-
let ident = format_ident!("{}{}ValidationError", ident, self.error_ident);
144+
let ident = format_error_ident_with_prefix(ident, &self.error_ident);
128145
let variant_ident = self.validations.iter().map(|validation| validation.ident());
129146
let variant_type = self
130147
.validations
@@ -194,3 +211,11 @@ fn upper_camel_ident(ident: &Ident) -> Ident {
194211
format_ident!("{}", s.to_case(Case::UpperCamel))
195212
}
196213
}
214+
215+
pub fn format_error_ident(ident: &Ident) -> Ident {
216+
format_ident!("{}ValidationError", ident)
217+
}
218+
219+
pub fn format_error_ident_with_prefix(prefix: &Ident, ident: &Ident) -> Ident {
220+
format_ident!("{}{}ValidationError", prefix, ident)
221+
}

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
use proc_macro2::{Literal, TokenStream};
2-
use quote::{ToTokens, format_ident, quote};
2+
use quote::{ToTokens, quote};
33
use syn::{Fields, FieldsNamed, FieldsUnnamed, Ident, Result, Visibility};
44

55
use crate::{
66
validate::{
77
attributes::enum_attributes,
8-
field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix},
8+
field::{LiteralOrIdent, ValidateField, ValidateFieldPrefix, format_error_ident},
99
},
1010
validation::Execution,
1111
};
@@ -64,7 +64,7 @@ pub struct ValidateNamedFields<'a> {
6464

6565
impl<'a> ValidateNamedFields<'a> {
6666
fn parse(visibility: &'a Visibility, ident: Ident, fields: &'a FieldsNamed) -> Result<Self> {
67-
let error_ident = format_ident!("{}ValidationError", ident);
67+
let error_ident = format_error_ident(&ident);
6868

6969
let mut result = Self {
7070
visibility,
@@ -127,7 +127,7 @@ pub struct ValidateUnnamedFields<'a> {
127127

128128
impl<'a> ValidateUnnamedFields<'a> {
129129
fn parse(visibility: &'a Visibility, ident: Ident, fields: &'a FieldsUnnamed) -> Result<Self> {
130-
let error_ident = format_ident!("{}ValidationError", ident);
130+
let error_ident = format_error_ident(&ident);
131131

132132
let mut result = Self {
133133
visibility,

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

Lines changed: 61 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,24 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{ToTokens, quote};
13
use syn::{GenericArgument, Path, PathArguments, Type, TypeParamBound};
24

5+
use crate::validate::field::format_error_ident;
6+
37
const PRIMITIVE_AND_BUILT_IN_TYPES: [&str; 18] = [
48
"bool", "i8", "i16", "i32", "i64", "i128", "isize", "u8", "u16", "u32", "u64", "u128", "usize",
59
"f32", "f64", "char", "str", "String",
610
];
711

8-
const CONTAINER_TYPES: [&str; 20] = [
12+
const INDEXED_CONTAINER_TYPES: [&str; 16] = [
913
"Arc",
10-
"BTreeMap",
1114
"BTreeSet",
12-
"HashMap",
1315
"HashSet",
1416
"LinkedList",
1517
"Option",
1618
"Rc",
1719
"Vec",
1820
"VecDeque",
19-
"std::collections::BTreeMap",
2021
"std::collections::BTreeSet",
21-
"std::collections::HashMap",
2222
"std::collections::HashSet",
2323
"std::collections::LinkedList",
2424
"std::collections::VecDeque",
@@ -28,6 +28,13 @@ const CONTAINER_TYPES: [&str; 20] = [
2828
"std::vec::Vec",
2929
];
3030

31+
const KEYED_CONTAINER_TYPES: [&str; 4] = [
32+
"BTreeMap",
33+
"HashMap",
34+
"std::collections::BTreeMap",
35+
"std::collections::HashMap",
36+
];
37+
3138
fn path_to_string(path: &Path) -> String {
3239
// TODO: This is probably slow, replace with comparisons.
3340
path.segments
@@ -42,59 +49,88 @@ fn is_validate_path(path: &Path) -> bool {
4249
path_string == "Validate" || path_string == "fortifier::Validate"
4350
}
4451

45-
fn should_validate_generic_argument(arg: &GenericArgument) -> bool {
52+
fn should_validate_generic_argument(arg: &GenericArgument) -> Option<KnownOrUnknown<TokenStream>> {
4653
match arg {
47-
GenericArgument::Lifetime(_) => true,
54+
GenericArgument::Lifetime(_) => Some(KnownOrUnknown::Unknown),
4855
GenericArgument::Type(r#type) => should_validate_type(r#type),
4956
GenericArgument::Const(_expr) => todo!(),
5057
GenericArgument::AssocType(_assoc_type) => todo!(),
5158
GenericArgument::AssocConst(_assoc_const) => todo!(),
5259
GenericArgument::Constraint(_constraint) => todo!(),
53-
_ => true,
60+
_ => Some(KnownOrUnknown::Unknown),
5461
}
5562
}
5663

57-
fn should_validate_path(path: &Path) -> bool {
64+
fn should_validate_path(path: &Path) -> Option<KnownOrUnknown<TokenStream>> {
5865
if let Some(ident) = path.get_ident() {
59-
return !PRIMITIVE_AND_BUILT_IN_TYPES.contains(&ident.to_string().as_str());
66+
return if PRIMITIVE_AND_BUILT_IN_TYPES.contains(&ident.to_string().as_str()) {
67+
None
68+
} else {
69+
Some(KnownOrUnknown::Known(
70+
format_error_ident(ident).to_token_stream(),
71+
))
72+
};
6073
}
6174
let path_string = path_to_string(path);
75+
let path_string = path_string.as_str();
6276

63-
if CONTAINER_TYPES.contains(&path_string.as_str())
77+
if INDEXED_CONTAINER_TYPES.contains(&path_string)
6478
&& let Some(segment) = path.segments.last()
6579
&& let PathArguments::AngleBracketed(arguments) = &segment.arguments
66-
&& !arguments.args.iter().all(should_validate_generic_argument)
80+
&& let Some(argument) = arguments.args.first()
6781
{
68-
return false;
82+
return should_validate_generic_argument(argument).map(|error_type| match error_type {
83+
KnownOrUnknown::Known(error_type) => KnownOrUnknown::Known(
84+
quote!(::fortifier::ValidationErrors<::fortifier::IndexedValidationError<#error_type>>)
85+
),
86+
KnownOrUnknown::Unknown => KnownOrUnknown::Unknown
87+
});
6988
}
7089

71-
true
90+
// TODO: Determine error type.
91+
if KEYED_CONTAINER_TYPES.contains(&path_string)
92+
&& let Some(segment) = path.segments.last()
93+
&& let PathArguments::AngleBracketed(arguments) = &segment.arguments
94+
&& !arguments
95+
.args
96+
.iter()
97+
.all(|arg| should_validate_generic_argument(arg).is_some())
98+
{
99+
return None;
100+
}
101+
102+
Some(KnownOrUnknown::Unknown)
103+
}
104+
105+
pub enum KnownOrUnknown<T> {
106+
Known(T),
107+
Unknown,
72108
}
73109

74-
pub fn should_validate_type(r#type: &Type) -> bool {
110+
pub fn should_validate_type(r#type: &Type) -> Option<KnownOrUnknown<TokenStream>> {
75111
match r#type {
76112
Type::Array(r#type) => should_validate_type(&r#type.elem),
77-
Type::BareFn(_) => false,
113+
Type::BareFn(_) => None,
78114
Type::Group(r#type) => should_validate_type(&r#type.elem),
79115
Type::ImplTrait(r#type) => r#type.bounds.iter().any(
80116
|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,
117+
).then_some(KnownOrUnknown::Unknown),
118+
Type::Infer(_) => Some(KnownOrUnknown::Unknown),
119+
Type::Macro(_) => Some(KnownOrUnknown::Unknown),
120+
Type::Never(_) => None,
85121
Type::Paren(r#type) => should_validate_type(&r#type.elem),
86122
Type::Path(r#type) => should_validate_path(&r#type.path),
87123
Type::Ptr(r#type) => should_validate_type(&r#type.elem),
88124
Type::Reference(r#type) => should_validate_type(&r#type.elem),
89125
Type::Slice(r#type) => should_validate_type(&r#type.elem),
90126
Type::TraitObject(r#type) => r#type.bounds.iter().any(
91127
|bound| matches!(bound, TypeParamBound::Trait(bound) if is_validate_path(&bound.path)),
92-
),
128+
).then_some(KnownOrUnknown::Unknown),
93129
Type::Tuple(r#type) => {
94-
!r#type.elems.is_empty() && r#type.elems.iter().all(should_validate_type)
130+
(!r#type.elems.is_empty() && r#type.elems.iter().all(|r#type| should_validate_type(r#type).is_some())).then_some(KnownOrUnknown::Unknown)
95131
}
96-
Type::Verbatim(_) => false,
97-
_ => false,
132+
Type::Verbatim(_) => None,
133+
_ => None,
98134
}
99135
}
100136

@@ -106,7 +142,7 @@ mod tests {
106142
use super::should_validate_type;
107143

108144
fn validate(tokens: TokenStream) -> bool {
109-
should_validate_type(&syn::parse2(tokens).expect("valid type"))
145+
should_validate_type(&syn::parse2(tokens).expect("valid type")).is_some()
110146
}
111147

112148
#[test]

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

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

55
use crate::validation::{Execution, Validation};
66

77
#[derive(Default)]
8-
pub struct Nested {}
8+
pub struct Nested {
9+
error_type: TokenStream,
10+
}
911

1012
impl Nested {
11-
pub fn new() -> Self {
12-
Self {}
13+
pub fn new(error_type: TokenStream) -> Self {
14+
Self { error_type }
1315
}
1416
}
1517

1618
impl Validation for Nested {
17-
fn parse(_meta: &ParseNestedMeta<'_>) -> Result<Self> {
18-
unimplemented!()
19+
fn parse(meta: &ParseNestedMeta<'_>) -> Result<Self> {
20+
let mut error_type: Option<Path> = None;
21+
22+
meta.parse_nested_meta(|meta| {
23+
if meta.path.is_ident("error_type") {
24+
error_type = Some(meta.value()?.parse()?);
25+
26+
Ok(())
27+
} else {
28+
Err(meta.error("unknown parameter"))
29+
}
30+
})?;
31+
32+
let Some(error_type) = error_type else {
33+
return Err(meta.error("missing `error_type` parameter"));
34+
};
35+
36+
Ok(Nested {
37+
error_type: error_type.to_token_stream(),
38+
})
1939
}
2040

2141
fn ident(&self) -> Ident {
2242
format_ident!("Nested")
2343
}
2444

2545
fn error_type(&self) -> TokenStream {
26-
// TODO
27-
quote!(::fortifier::NestedError)
46+
self.error_type.clone()
2847
}
2948

3049
fn expr(&self, execution: Execution, expr: &TokenStream) -> Option<TokenStream> {

0 commit comments

Comments
 (0)