Skip to content

Commit bc5a778

Browse files
feat: add custom validation
1 parent 99cc4c2 commit bc5a778

7 files changed

Lines changed: 133 additions & 4 deletions

File tree

Cargo.lock

Lines changed: 28 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ version = "0.0.1"
1212
[workspace.dependencies]
1313
fortifier = { path = "./packages/fortifier", version = "0.0.1" }
1414
fortifier-macros = { path = "./packages/fortifier-macros", version = "0.0.1" }
15+
tokio = "1.48.0"
1516

1617
[workspace.lints.rust]
1718
unsafe_code = "deny"

example/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ version.workspace = true
1010

1111
[dependencies]
1212
fortifier.workspace = true
13+
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
1314

1415
[lints]
1516
workspace = true

example/src/main.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,32 @@ struct CreateUser {
1212

1313
#[validate(url)]
1414
url: String,
15+
16+
#[validate(custom(function = validate_one_locale_required, error = OneLocaleRequiredError))]
17+
locales: Vec<String>,
18+
}
19+
20+
#[derive(Debug)]
21+
struct OneLocaleRequiredError;
22+
23+
fn validate_one_locale_required(locales: &[String]) -> Result<(), OneLocaleRequiredError> {
24+
if locales.is_empty() {
25+
Err(OneLocaleRequiredError)
26+
} else {
27+
Ok(())
28+
}
1529
}
1630

17-
fn main() -> Result<(), Box<dyn Error>> {
31+
#[tokio::main]
32+
async fn main() -> Result<(), Box<dyn Error>> {
1833
let data = CreateUser {
1934
email: "john@doe.com".to_owned(),
2035
name: "John Doe".to_owned(),
2136
url: "https://john.doe.com".to_owned(),
37+
locales: vec!["en_GB".to_owned()],
2238
};
2339

24-
data.validate_sync()?;
40+
data.validate().await?;
2541

2642
Ok(())
2743
}

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

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ use syn::{Field, Result};
44

55
use crate::{
66
validation::Validation,
7-
validations::{Email, Length, Url},
7+
validations::{Custom, Email, Length, Url},
88
};
99

1010
pub struct ValidateField {
@@ -22,7 +22,11 @@ impl ValidateField {
2222
for attr in &field.attrs {
2323
if attr.path().is_ident("validate") {
2424
attr.parse_nested_meta(|meta| {
25-
if meta.path.is_ident("email") {
25+
if meta.path.is_ident("custom") {
26+
result.validations.push(Box::new(Custom::parse(&meta)?));
27+
28+
Ok(())
29+
} else if meta.path.is_ident("email") {
2630
result.validations.push(Box::new(Email::parse(&meta)?));
2731

2832
Ok(())
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1+
mod custom;
12
mod email;
23
mod length;
34
mod url;
45

6+
pub use custom::*;
57
pub use email::*;
68
pub use length::*;
79
pub use url::*;
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{ToTokens, quote};
3+
use syn::{LitBool, Path, Result, Type, meta::ParseNestedMeta};
4+
5+
use crate::validation::Validation;
6+
7+
pub struct Custom {
8+
is_async: bool,
9+
error_type: Type,
10+
function_path: Path,
11+
}
12+
13+
impl Validation for Custom {
14+
fn parse(meta: &ParseNestedMeta<'_>) -> Result<Self> {
15+
let mut is_async = false;
16+
let mut error_type: Option<Type> = None;
17+
let mut function_path: Option<Path> = None;
18+
19+
meta.parse_nested_meta(|meta| {
20+
if meta.path.is_ident("async") {
21+
if let Ok(value) = meta.value() {
22+
let lit: LitBool = value.parse()?;
23+
is_async = lit.value;
24+
} else {
25+
is_async = true;
26+
}
27+
28+
Ok(())
29+
} else if meta.path.is_ident("error") {
30+
error_type = Some(meta.value()?.parse()?);
31+
32+
Ok(())
33+
} else if meta.path.is_ident("function") {
34+
function_path = Some(meta.value()?.parse()?);
35+
36+
Ok(())
37+
} else {
38+
Err(meta.error("unknown parameter"))
39+
}
40+
})?;
41+
42+
let Some(error_type) = error_type else {
43+
return Err(meta.error("missing error parameter"));
44+
};
45+
let Some(function_path) = function_path else {
46+
return Err(meta.error("missing function parameter"));
47+
};
48+
49+
Ok(Custom {
50+
is_async,
51+
error_type,
52+
function_path,
53+
})
54+
}
55+
56+
fn is_async(&self) -> bool {
57+
self.is_async
58+
}
59+
60+
fn error_type(&self) -> TokenStream {
61+
self.error_type.to_token_stream()
62+
}
63+
64+
fn tokens(&self, expr: &TokenStream) -> TokenStream {
65+
let function_path = &self.function_path;
66+
67+
if self.is_async {
68+
quote! {
69+
#function_path(&#expr).await
70+
}
71+
} else {
72+
quote! {
73+
#function_path(&#expr)
74+
}
75+
}
76+
}
77+
}

0 commit comments

Comments
 (0)