Skip to content

Commit 65477d7

Browse files
committed
Add @regex directive
1 parent 93d8db9 commit 65477d7

File tree

3 files changed

+189
-0
lines changed

3 files changed

+189
-0
lines changed

aiscript-directive/src/validator/mod.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
11
use std::any::Any;
22

33
use date::DateValidator;
4+
use regex::RegexValidator;
45
use serde_json::Value;
56

67
use crate::{Directive, DirectiveParams, FromDirective};
78

89
mod array;
910
mod date;
1011
mod format;
12+
mod regex;
1113

1214
pub trait Validator: Send + Sync + Any {
1315
fn name(&self) -> &'static str;
@@ -265,6 +267,7 @@ impl FromDirective for Box<dyn Validator> {
265267
"not" => Ok(Box::new(NotValidator::from_directive(directive)?)),
266268
"date" => Ok(Box::new(DateValidator::from_directive(directive)?)),
267269
"array" => Ok(Box::new(AnyValidator::from_directive(directive)?)), // Add this line
270+
"regex" => Ok(Box::new(RegexValidator::from_directive(directive)?)), // Add support for regex directive
268271
v => Err(format!("Invalid validators: @{}", v)),
269272
}
270273
}
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
use regex::Regex;
2+
use serde_json::Value;
3+
use std::any::Any;
4+
5+
use super::Validator;
6+
use crate::{Directive, DirectiveParams, FromDirective};
7+
8+
pub struct RegexValidator {
9+
pattern: Regex,
10+
raw_pattern: String,
11+
}
12+
13+
impl Validator for RegexValidator {
14+
fn name(&self) -> &'static str {
15+
"@regex"
16+
}
17+
18+
fn validate(&self, value: &Value) -> Result<(), String> {
19+
let value_str = match value.as_str() {
20+
Some(s) => s,
21+
None => return Err("Value must be a string".into()),
22+
};
23+
24+
if self.pattern.is_match(value_str) {
25+
Ok(())
26+
} else {
27+
Err(format!(
28+
"Value does not match the regex pattern: {}",
29+
self.raw_pattern
30+
))
31+
}
32+
}
33+
34+
fn as_any(&self) -> &dyn Any {
35+
self
36+
}
37+
}
38+
39+
impl FromDirective for RegexValidator {
40+
fn from_directive(directive: Directive) -> Result<Self, String> {
41+
// Only support KeyValue format with "pattern" parameter
42+
match &directive.params {
43+
DirectiveParams::KeyValue(params) => {
44+
// Get the pattern parameter
45+
let pattern_str = params
46+
.get("pattern")
47+
.and_then(|v| v.as_str())
48+
.ok_or_else(|| "@regex directive requires a 'pattern' parameter".to_string())?;
49+
50+
// Compile the regex
51+
let regex = match Regex::new(pattern_str) {
52+
Ok(re) => re,
53+
Err(e) => return Err(format!("Invalid regex pattern: {}", e)),
54+
};
55+
56+
Ok(Self {
57+
pattern: regex,
58+
raw_pattern: pattern_str.to_string(),
59+
})
60+
}
61+
_ => {
62+
Err("Invalid format for @regex directive. Use @regex(pattern=\"...\")".to_string())
63+
}
64+
}
65+
}
66+
}
67+
68+
#[cfg(test)]
69+
mod tests {
70+
use super::*;
71+
use crate::{Directive, DirectiveParams};
72+
use serde_json::json;
73+
use std::collections::HashMap;
74+
75+
fn create_directive(pattern: &str) -> Directive {
76+
let mut params = HashMap::new();
77+
params.insert("pattern".to_string(), json!(pattern));
78+
79+
Directive {
80+
name: "regex".into(),
81+
params: DirectiveParams::KeyValue(params),
82+
line: 1,
83+
}
84+
}
85+
86+
#[test]
87+
fn test_regex_validator_basic() {
88+
let directive = create_directive("^[a-z]+$");
89+
let validator = RegexValidator::from_directive(directive).unwrap();
90+
91+
assert!(validator.validate(&json!("abc")).is_ok());
92+
assert!(validator.validate(&json!("123")).is_err());
93+
assert!(validator.validate(&json!("abc123")).is_err());
94+
assert!(validator.validate(&json!("ABC")).is_err());
95+
}
96+
97+
#[test]
98+
fn test_regex_validator_for_ssn() {
99+
let directive = create_directive("^\\d{3}-\\d{2}-\\d{4}$");
100+
let validator = RegexValidator::from_directive(directive).unwrap();
101+
102+
assert!(validator.validate(&json!("123-45-6789")).is_ok());
103+
assert!(validator.validate(&json!("abc-12-3456")).is_err());
104+
assert!(validator.validate(&json!("12-34-5678")).is_err());
105+
assert!(validator.validate(&json!("1234-56-7890")).is_err());
106+
}
107+
108+
#[test]
109+
fn test_regex_validator_with_non_string_value() {
110+
let directive = create_directive("^[a-z]+$");
111+
let validator = RegexValidator::from_directive(directive).unwrap();
112+
113+
assert!(validator.validate(&json!(123)).is_err());
114+
assert!(validator.validate(&json!(true)).is_err());
115+
assert!(validator.validate(&json!(null)).is_err());
116+
}
117+
118+
#[test]
119+
fn test_invalid_regex_pattern() {
120+
let directive = create_directive("*invalid*");
121+
assert!(RegexValidator::from_directive(directive).is_err());
122+
}
123+
124+
#[test]
125+
fn test_missing_pattern() {
126+
// Empty HashMap for parameters
127+
let params = HashMap::new();
128+
let directive = Directive {
129+
name: "regex".into(),
130+
params: DirectiveParams::KeyValue(params),
131+
line: 1,
132+
};
133+
assert!(RegexValidator::from_directive(directive).is_err());
134+
}
135+
136+
#[test]
137+
fn test_incorrect_params_type() {
138+
// Test with Array instead of KeyValue params
139+
let directive = Directive {
140+
name: "regex".into(),
141+
params: DirectiveParams::Array(vec![json!("^[a-z]+$")]),
142+
line: 1,
143+
};
144+
assert!(RegexValidator::from_directive(directive).is_err());
145+
}
146+
147+
#[test]
148+
fn test_complex_regex() {
149+
let directive = create_directive("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}$");
150+
let validator = RegexValidator::from_directive(directive).unwrap();
151+
152+
assert!(validator.validate(&json!("[email protected]")).is_ok());
153+
assert!(
154+
validator
155+
.validate(&json!("[email protected]"))
156+
.is_ok()
157+
);
158+
assert!(validator.validate(&json!("invalid-email")).is_err());
159+
assert!(validator.validate(&json!("missing@domain")).is_err());
160+
}
161+
}

examples/routes/regex.ai

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
post /api/register {
2+
@json
3+
body {
4+
"""Username validation (alphanumeric and underscore only)"""
5+
@regex(pattern="^[a-zA-Z0-9_]+$")
6+
username: str,
7+
8+
"""SSN validation (XXX-XX-XXXX format)"""
9+
@regex(pattern="^\d{3}-\d{2}-\d{4}$")
10+
ssn: str
11+
}
12+
13+
print("Received registration for: ", username);
14+
return { "success": true };
15+
}
16+
17+
get /api/validate_phone {
18+
query {
19+
"""Phone number validation ((XXX)-XXX-XXXX format)"""
20+
@regex(pattern="^\d{3}-\d{3}-\d{4}$")
21+
phone: str
22+
}
23+
24+
return { "valid": true };
25+
}

0 commit comments

Comments
 (0)