Skip to content

Commit 9d29322

Browse files
feat: strip options from custom validation (#65)
1 parent e400dcc commit 9d29322

7 files changed

Lines changed: 266 additions & 68 deletions

File tree

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

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@ use trybuild::TestCases;
33
#[test]
44
fn validations() {
55
let t = TestCases::new();
6-
t.pass("tests/validations/*/root_generics_pass.rs");
7-
// t.pass("tests/validations/*/*_pass.rs");
8-
// t.compile_fail("tests/validations/*/*_fail.rs");
6+
t.pass("tests/validations/*/*_pass.rs");
7+
t.compile_fail("tests/validations/*/*_fail.rs");
98
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
use fortifier::{Validate, error_code};
2+
use serde::{Deserialize, Serialize};
3+
4+
#[derive(Deserialize, Serialize, Validate)]
5+
struct CustomData<'a> {
6+
#[validate(custom(function = custom, error = CustomError))]
7+
zero_options: &'a str,
8+
9+
#[validate(custom(function = custom, error = CustomError))]
10+
strip_one_option: Option<&'a str>,
11+
#[validate(custom(function = custom, error = CustomError))]
12+
strip_two_options: Option<Option<&'a str>>,
13+
#[validate(custom(function = custom, error = CustomError))]
14+
strip_three_options: Option<Option<Option<&'a str>>>,
15+
16+
#[validate(custom(function = custom_one_option, error = CustomError, options))]
17+
strip_no_options_from_one: Option<&'a str>,
18+
#[validate(custom(function = custom_two_options, error = CustomError, options))]
19+
strip_no_options_from_two: Option<Option<&'a str>>,
20+
#[validate(custom(function = custom_three_options, error = CustomError, options))]
21+
strip_no_options_from_three: Option<Option<Option<&'a str>>>,
22+
23+
#[validate(custom(function = custom_one_option, error = CustomError, options = 1))]
24+
strip_to_one_option_from_one: Option<&'a str>,
25+
#[validate(custom(function = custom_one_option, error = CustomError, options = 1))]
26+
strip_to_one_option_from_two: Option<Option<&'a str>>,
27+
#[validate(custom(function = custom_one_option, error = CustomError, options = 1))]
28+
strip_to_one_option_from_three: Option<Option<Option<&'a str>>>,
29+
30+
#[validate(custom(function = custom_one_option, error = CustomError, options = 2))]
31+
strip_to_two_options_from_one: Option<&'a str>,
32+
#[validate(custom(function = custom_two_options, error = CustomError, options = 2))]
33+
strip_to_two_options_from_two: Option<Option<&'a str>>,
34+
#[validate(custom(function = custom_two_options, error = CustomError, options = 2))]
35+
strip_to_two_options_from_three: Option<Option<Option<&'a str>>>,
36+
}
37+
38+
error_code!(CustomErrorCode, "custom");
39+
40+
#[derive(Debug, Deserialize, PartialEq, Serialize)]
41+
#[serde(rename_all = "camelCase")]
42+
struct CustomError {
43+
code: CustomErrorCode,
44+
}
45+
46+
fn custom(value: &str) -> Result<(), CustomError> {
47+
if value == "" {
48+
Ok(())
49+
} else {
50+
Err(CustomError {
51+
code: CustomErrorCode,
52+
})
53+
}
54+
}
55+
56+
fn custom_one_option(value: &Option<&str>) -> Result<(), CustomError> {
57+
if let Some(value) = value
58+
&& *value == ""
59+
{
60+
Ok(())
61+
} else {
62+
Err(CustomError {
63+
code: CustomErrorCode,
64+
})
65+
}
66+
}
67+
68+
fn custom_two_options(value: &Option<Option<&str>>) -> Result<(), CustomError> {
69+
if let Some(Some(value)) = value
70+
&& *value == ""
71+
{
72+
Ok(())
73+
} else {
74+
Err(CustomError {
75+
code: CustomErrorCode,
76+
})
77+
}
78+
}
79+
80+
fn custom_three_options(value: &Option<Option<Option<&str>>>) -> Result<(), CustomError> {
81+
if let Some(Some(Some(value))) = value
82+
&& *value == ""
83+
{
84+
Ok(())
85+
} else {
86+
Err(CustomError {
87+
code: CustomErrorCode,
88+
})
89+
}
90+
}
91+
92+
fn main() {
93+
let data = CustomData {
94+
zero_options: "",
95+
96+
strip_one_option: Some(""),
97+
strip_two_options: Some(Some("")),
98+
strip_three_options: Some(Some(Some(""))),
99+
100+
strip_no_options_from_one: Some(""),
101+
strip_no_options_from_two: Some(Some("")),
102+
strip_no_options_from_three: Some(Some(Some(""))),
103+
104+
strip_to_one_option_from_one: Some(""),
105+
strip_to_one_option_from_two: Some(Some("")),
106+
strip_to_one_option_from_three: Some(Some(Some(""))),
107+
108+
strip_to_two_options_from_one: Some(""),
109+
strip_to_two_options_from_two: Some(Some("")),
110+
strip_to_two_options_from_three: Some(Some(Some(""))),
111+
};
112+
113+
assert_eq!(data.validate_sync(), Ok(()));
114+
}

packages/fortifier-macros/src/util.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use convert_case::{Case, Casing};
22
use quote::format_ident;
3-
use syn::Ident;
3+
use syn::{GenericArgument, Ident, Path, PathArguments, Type};
44

55
pub fn upper_camel_ident(ident: &Ident) -> Ident {
66
let s = ident.to_string();
@@ -11,3 +11,30 @@ pub fn upper_camel_ident(ident: &Ident) -> Ident {
1111
format_ident!("{}", s.to_case(Case::UpperCamel))
1212
}
1313
}
14+
15+
pub fn path_to_string(path: &Path) -> String {
16+
path.segments
17+
.iter()
18+
.map(|segment| segment.ident.to_string())
19+
.collect::<Vec<_>>()
20+
.join("::")
21+
}
22+
23+
pub fn is_option_path(path: &Path) -> bool {
24+
let path_string = path_to_string(path);
25+
path_string == "Option" || path_string == "std::option::Option"
26+
}
27+
28+
pub fn count_options(r#type: &Type) -> usize {
29+
if let Type::Path(r#type) = r#type
30+
&& let Some(segment) = r#type.path.segments.last()
31+
&& let PathArguments::AngleBracketed(arguments) = &segment.arguments
32+
&& arguments.args.len() == 1
33+
&& is_option_path(&r#type.path)
34+
&& let Some(GenericArgument::Type(argument_type)) = arguments.args.first()
35+
{
36+
1 + count_options(argument_type)
37+
} else {
38+
0
39+
}
40+
}

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ use syn::{
55
TypeParamBound, WherePredicate, punctuated::Punctuated, token::PathSep,
66
};
77

8-
use crate::{integrations::where_predicate, validate::error::format_error_ident};
8+
use crate::{
9+
integrations::where_predicate, util::path_to_string, validate::error::format_error_ident,
10+
};
911

1012
/// Primitive and built-in types.
1113
///
@@ -244,14 +246,6 @@ impl ValidateResult {
244246
}
245247
}
246248

247-
fn path_to_string(path: &Path) -> String {
248-
path.segments
249-
.iter()
250-
.map(|segment| segment.ident.to_string())
251-
.collect::<Vec<_>>()
252-
.join("::")
253-
}
254-
255249
fn is_validate_path(path: &Path) -> bool {
256250
let path_string = path_to_string(path);
257251
path_string == "Validate"

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

Lines changed: 64 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,31 @@
11
use proc_macro2::TokenStream;
22
use quote::{ToTokens, format_ident, quote};
3-
use syn::{Ident, LitBool, Path, Result, Type, TypePath, meta::ParseNestedMeta};
3+
use syn::{Ident, LitBool, LitInt, Path, Result, Type, TypePath, meta::ParseNestedMeta};
44

55
use crate::{
66
generics::{Generic, generic_arguments},
7-
util::upper_camel_ident,
7+
util::{count_options, upper_camel_ident},
88
validation::{Execution, Validation},
99
};
1010

1111
pub struct Custom {
12+
r#type: Type,
1213
name: Ident,
13-
execution: Execution,
1414
error_type: TypePath,
1515
function_path: Path,
16+
execution: Execution,
1617
context: bool,
18+
max_options: usize,
1719
}
1820

1921
impl Validation for Custom {
20-
fn parse(_type: &Type, meta: &ParseNestedMeta<'_>) -> Result<Self> {
22+
fn parse(r#type: &Type, meta: &ParseNestedMeta<'_>) -> Result<Self> {
2123
let mut name = None;
22-
let mut execution = Execution::Sync;
2324
let mut error_type: Option<TypePath> = None;
2425
let mut function_path: Option<Path> = None;
26+
let mut execution = Execution::Sync;
2527
let mut context = false;
28+
let mut max_options = 0;
2629

2730
meta.parse_nested_meta(|meta| {
2831
if meta.path.is_ident("async") {
@@ -54,6 +57,15 @@ impl Validation for Custom {
5457
} else if meta.path.is_ident("function") {
5558
function_path = Some(meta.value()?.parse()?);
5659

60+
Ok(())
61+
} else if meta.path.is_ident("options") {
62+
if let Ok(value) = meta.value() {
63+
let lit: LitInt = value.parse()?;
64+
max_options = lit.base10_parse::<usize>()?;
65+
} else {
66+
max_options = usize::MAX;
67+
}
68+
5769
Ok(())
5870
} else if meta.path.is_ident("name") {
5971
let ident = meta.value()?.parse()?;
@@ -78,11 +90,13 @@ impl Validation for Custom {
7890
});
7991

8092
Ok(Custom {
93+
r#type: r#type.clone(),
8194
name,
82-
execution,
8395
error_type,
8496
function_path,
97+
execution,
8598
context,
99+
max_options,
86100
})
87101
}
88102

@@ -103,24 +117,53 @@ impl Validation for Custom {
103117
}
104118

105119
fn expr(&self, execution: Execution, expr: &TokenStream) -> Option<TokenStream> {
106-
let context_expr = self.context.then(|| quote!(, &context));
107-
108120
match (execution, self.execution) {
109-
(Execution::Sync, Execution::Sync) => {
110-
let function_path = &self.function_path;
121+
(Execution::Sync, Execution::Sync) => Some(wrapper(
122+
&self.r#type,
123+
&self.function_path,
124+
expr,
125+
self.context,
126+
self.max_options,
127+
None,
128+
)),
129+
(Execution::Async, Execution::Async) => Some(wrapper(
130+
&self.r#type,
131+
&self.function_path,
132+
expr,
133+
self.context,
134+
self.max_options,
135+
Some(quote!(.await)),
136+
)),
137+
_ => None,
138+
}
139+
}
140+
}
111141

112-
Some(quote! {
113-
#function_path(&#expr #context_expr)
114-
})
115-
}
116-
(Execution::Async, Execution::Async) => {
117-
let function_path = &self.function_path;
142+
fn wrapper(
143+
r#type: &Type,
144+
function_path: &Path,
145+
expr: &TokenStream,
146+
context: bool,
147+
max_options: usize,
148+
suffix: Option<TokenStream>,
149+
) -> TokenStream {
150+
let context_expr = context.then(|| quote!(, &context));
151+
152+
let count = count_options(r#type);
153+
let remove_count = count.saturating_sub(max_options);
154+
155+
if remove_count > 0 {
156+
let mut wrapper = quote!(value);
157+
for _ in 0..remove_count {
158+
wrapper = quote!(Some(#wrapper));
159+
}
118160

119-
Some(quote! {
120-
#function_path(&#expr #context_expr).await
121-
})
122-
}
123-
_ => None,
161+
quote! {
162+
{ if let #wrapper = &#expr { #function_path(value #context_expr) #suffix} else { Ok(())} }
163+
}
164+
} else {
165+
quote! {
166+
#function_path(&#expr #context_expr) #suffix
124167
}
125168
}
126169
}

0 commit comments

Comments
 (0)