Skip to content

Commit a4cdec9

Browse files
feat: add regex validation
1 parent 91bc662 commit a4cdec9

11 files changed

Lines changed: 330 additions & 5 deletions

File tree

Cargo.lock

Lines changed: 40 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+
regex = "1.12.2"
1516
tokio = "1.48.0"
1617

1718
[workspace.lints.rust]

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+
regex.workspace = true
1314
tokio = { workspace = true, features = ["macros", "rt-multi-thread"] }
1415

1516
[lints]

example/src/main.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
use std::error::Error;
1+
use std::{error::Error, sync::LazyLock};
22

33
use fortifier::Validate;
4+
use regex::Regex;
5+
6+
static COUNTRY_CODE_REGEX: LazyLock<Regex> =
7+
LazyLock::new(|| Regex::new(r"[A-Z]{2}").expect("Regex should be valid."));
48

59
#[derive(Validate)]
610
struct CreateUser {
@@ -13,6 +17,9 @@ struct CreateUser {
1317
#[validate(url)]
1418
url: String,
1519

20+
#[validate(regex(expr = &COUNTRY_CODE_REGEX))]
21+
country_code: String,
22+
1623
#[validate(custom(function = validate_one_locale_required, error = OneLocaleRequiredError))]
1724
#[validate(length(min = 1))]
1825
locales: Vec<String>,
@@ -35,6 +42,7 @@ async fn main() -> Result<(), Box<dyn Error>> {
3542
email: "john@doe.com".to_owned(),
3643
name: "John Doe".to_owned(),
3744
url: "https://john.doe.com".to_owned(),
45+
country_code: "GB".to_owned(),
3846
locales: vec!["en_GB".to_owned()],
3947
};
4048

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

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

66
use crate::{
77
validation::Validation,
8-
validations::{Custom, Email, Length, Url},
8+
validations::{Custom, Email, Length, Regex, Url},
99
};
1010

1111
pub struct ValidateField {
@@ -44,13 +44,17 @@ impl ValidateField {
4444
} else if meta.path.is_ident("length") {
4545
result.validations.push(Box::new(Length::parse(&meta)?));
4646

47+
Ok(())
48+
} else if meta.path.is_ident("regex") {
49+
result.validations.push(Box::new(Regex::parse(&meta)?));
50+
4751
Ok(())
4852
} else if meta.path.is_ident("url") {
4953
result.validations.push(Box::new(Url::parse(&meta)?));
5054

5155
Ok(())
5256
} else {
53-
Err(meta.error("unknown validate parameter"))
57+
Err(meta.error("unknown parameter"))
5458
}
5559
})?;
5660
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
mod custom;
22
mod email;
33
mod length;
4+
mod regex;
45
mod url;
56

67
pub use custom::*;
78
pub use email::*;
89
pub use length::*;
10+
pub use regex::*;
911
pub use url::*;

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ impl Validation for Length {
3232

3333
Ok(())
3434
} else {
35-
Err(meta.error("unknown length parameter"))
35+
Err(meta.error("unknown parameter"))
3636
}
3737
})?;
3838

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
use proc_macro2::TokenStream;
2+
use quote::{format_ident, quote};
3+
use syn::{Expr, Ident, Result, meta::ParseNestedMeta};
4+
5+
use crate::validation::Validation;
6+
7+
pub struct Regex {
8+
pub expr: Expr,
9+
}
10+
11+
impl Validation for Regex {
12+
fn parse(meta: &ParseNestedMeta<'_>) -> Result<Self> {
13+
let mut expr: Option<Expr> = None;
14+
15+
meta.parse_nested_meta(|meta| {
16+
if meta.path.is_ident("expr") {
17+
expr = Some(meta.value()?.parse()?);
18+
19+
Ok(())
20+
} else {
21+
Err(meta.error("unknown parameter"))
22+
}
23+
})?;
24+
25+
let Some(expr) = expr else {
26+
return Err(meta.error("missing expr parameter"));
27+
};
28+
29+
Ok(Regex { expr })
30+
}
31+
32+
fn is_async(&self) -> bool {
33+
false
34+
}
35+
36+
fn ident(&self) -> Ident {
37+
format_ident!("Regex")
38+
}
39+
40+
fn error_type(&self) -> TokenStream {
41+
quote!(RegexError)
42+
}
43+
44+
fn tokens(&self, expr: &TokenStream) -> TokenStream {
45+
let regex_expr = &self.expr;
46+
47+
quote! {
48+
#expr.validate_regex(#regex_expr)
49+
}
50+
}
51+
}

packages/fortifier/Cargo.toml

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@ repository.workspace = true
99
version.workspace = true
1010

1111
[features]
12-
default = ["macros", "url"]
12+
default = ["macros", "regex", "url"]
1313
indexmap = ["dep:indexmap"]
1414
macros = ["dep:fortifier-macros"]
15+
regex = ["dep:regex"]
1516
url = ["dep:url"]
1617

1718
[dependencies]
1819
fortifier-macros = { workspace = true, optional = true }
1920
indexmap = { version = "2.12.0", optional = true }
21+
regex = { workspace = true, optional = true }
2022
url = { version = "2.5.7", optional = true }
2123

2224
[lints]
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
mod email;
22
mod length;
3+
#[cfg(feature = "regex")]
4+
mod regex;
35
#[cfg(feature = "url")]
46
mod url;
57

68
pub use email::*;
79
pub use length::*;
10+
#[cfg(feature = "regex")]
11+
pub use regex::*;
812
#[cfg(feature = "url")]
913
pub use url::*;

0 commit comments

Comments
 (0)