Skip to content

Commit f11208e

Browse files
committed
Add @Date directive
1 parent 4fd5271 commit f11208e

File tree

2 files changed

+277
-0
lines changed

2 files changed

+277
-0
lines changed
Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
use std::any::Any;
2+
3+
use chrono::NaiveDate;
4+
use serde_json::Value;
5+
6+
use crate::{Directive, DirectiveParams, FromDirective};
7+
8+
use super::Validator;
9+
10+
pub struct DateValidator {
11+
pub format: Option<String>,
12+
pub min: Option<String>,
13+
pub max: Option<String>,
14+
}
15+
16+
impl Validator for DateValidator {
17+
fn name(&self) -> &'static str {
18+
"@date"
19+
}
20+
21+
fn validate(&self, value: &Value) -> Result<(), String> {
22+
let value_str = match value.as_str() {
23+
Some(s) => s,
24+
None => return Err("Value must be a string".into()),
25+
};
26+
27+
// Get the format and convert it to chrono format
28+
let user_format = self.format.as_deref().unwrap_or("YYYY-MM-DD");
29+
let chrono_format = convert_format(user_format);
30+
31+
// Parse the date
32+
let date = match NaiveDate::parse_from_str(value_str, &chrono_format) {
33+
Ok(date) => date,
34+
Err(_) => return Err(format!("Invalid date format, expected {}", user_format)),
35+
};
36+
37+
// Check minimum date constraint
38+
if let Some(min_str) = &self.min {
39+
match NaiveDate::parse_from_str(min_str, &chrono_format) {
40+
Ok(min_date) => {
41+
if date < min_date {
42+
return Err(format!("Date must be on or after {}", min_str));
43+
}
44+
}
45+
Err(_) => return Err(format!("Invalid minimum date: {}", min_str)),
46+
}
47+
}
48+
49+
// Check maximum date constraint
50+
if let Some(max_str) = &self.max {
51+
match NaiveDate::parse_from_str(max_str, &chrono_format) {
52+
Ok(max_date) => {
53+
if date > max_date {
54+
return Err(format!("Date must be on or before {}", max_str));
55+
}
56+
}
57+
Err(_) => return Err(format!("Invalid maximum date: {}", max_str)),
58+
}
59+
}
60+
61+
Ok(())
62+
}
63+
64+
fn as_any(&self) -> &dyn Any {
65+
self
66+
}
67+
}
68+
69+
impl FromDirective for DateValidator {
70+
fn from_directive(directive: Directive) -> Result<Self, String> {
71+
match directive.params {
72+
DirectiveParams::KeyValue(params) => {
73+
let format = params
74+
.get("format")
75+
.and_then(|v| v.as_str().map(ToString::to_string));
76+
let min = params
77+
.get("min")
78+
.and_then(|v| v.as_str().map(ToString::to_string));
79+
let max = params
80+
.get("max")
81+
.and_then(|v| v.as_str().map(ToString::to_string));
82+
83+
// Validate that min and max follow the format if provided
84+
if let (Some(format_str), Some(min_str)) = (&format, &min) {
85+
let chrono_format = convert_format(format_str);
86+
if NaiveDate::parse_from_str(min_str, &chrono_format).is_err() {
87+
return Err(format!(
88+
"min date '{}' doesn't match format '{}'",
89+
min_str, format_str
90+
));
91+
}
92+
}
93+
94+
if let (Some(format_str), Some(max_str)) = (&format, &max) {
95+
let chrono_format = convert_format(format_str);
96+
if NaiveDate::parse_from_str(max_str, &chrono_format).is_err() {
97+
return Err(format!(
98+
"max date '{}' doesn't match format '{}'",
99+
max_str, format_str
100+
));
101+
}
102+
}
103+
104+
Ok(Self { format, min, max })
105+
}
106+
_ => Err("Invalid params for @date directive".into()),
107+
}
108+
}
109+
}
110+
111+
fn convert_format(user_format: &str) -> String {
112+
user_format
113+
.replace("YYYY", "%Y")
114+
.replace("YY", "%y")
115+
.replace("MM", "%m")
116+
.replace("DD", "%d")
117+
.replace("M", "%m")
118+
.replace("D", "%d")
119+
}
120+
121+
#[cfg(test)]
122+
mod tests {
123+
use super::*;
124+
use crate::{Directive, DirectiveParams};
125+
use serde_json::json;
126+
use std::collections::HashMap;
127+
128+
fn create_directive(params: HashMap<String, Value>) -> Directive {
129+
Directive {
130+
name: "date".into(),
131+
params: DirectiveParams::KeyValue(params),
132+
line: 1,
133+
}
134+
}
135+
136+
#[test]
137+
fn test_date_validator_basic() {
138+
let params = HashMap::new();
139+
let directive = create_directive(params);
140+
let validator = DateValidator::from_directive(directive).unwrap();
141+
142+
// Default format is %Y-%m-%d
143+
assert!(validator.validate(&json!("2023-05-15")).is_ok());
144+
assert!(validator.validate(&json!("not-a-date")).is_err());
145+
assert!(validator.validate(&json!("20230515")).is_err());
146+
assert!(validator.validate(&json!(123)).is_err());
147+
}
148+
149+
#[test]
150+
fn test_date_validator_with_format() {
151+
let mut params = HashMap::new();
152+
params.insert("format".into(), json!("MM/DD/YYYY"));
153+
let directive = create_directive(params);
154+
let validator = DateValidator::from_directive(directive).unwrap();
155+
156+
assert!(validator.validate(&json!("05/15/2023")).is_ok());
157+
assert!(validator.validate(&json!("2023-05-15")).is_err());
158+
}
159+
160+
#[test]
161+
fn test_date_validator_min_constraint() {
162+
let mut params = HashMap::new();
163+
params.insert("min".into(), json!("2023-01-01"));
164+
let directive = create_directive(params);
165+
let validator = DateValidator::from_directive(directive).unwrap();
166+
167+
assert!(validator.validate(&json!("2023-01-01")).is_ok()); // Equal to min
168+
assert!(validator.validate(&json!("2023-02-15")).is_ok()); // After min
169+
assert!(validator.validate(&json!("2022-12-31")).is_err()); // Before min
170+
}
171+
172+
#[test]
173+
fn test_date_validator_max_constraint() {
174+
let mut params = HashMap::new();
175+
params.insert("max".into(), json!("2023-12-31"));
176+
let directive = create_directive(params);
177+
let validator = DateValidator::from_directive(directive).unwrap();
178+
179+
assert!(validator.validate(&json!("2023-12-31")).is_ok()); // Equal to max
180+
assert!(validator.validate(&json!("2023-06-15")).is_ok()); // Before max
181+
assert!(validator.validate(&json!("2024-01-01")).is_err()); // After max
182+
}
183+
184+
#[test]
185+
fn test_date_validator_min_max_constraints() {
186+
let mut params = HashMap::new();
187+
params.insert("min".into(), json!("2023-01-01"));
188+
params.insert("max".into(), json!("2023-12-31"));
189+
let directive = create_directive(params);
190+
let validator = DateValidator::from_directive(directive).unwrap();
191+
192+
assert!(validator.validate(&json!("2023-01-01")).is_ok()); // Equal to min
193+
assert!(validator.validate(&json!("2023-12-31")).is_ok()); // Equal to max
194+
assert!(validator.validate(&json!("2023-06-15")).is_ok()); // Between min and max
195+
assert!(validator.validate(&json!("2022-12-31")).is_err()); // Before min
196+
assert!(validator.validate(&json!("2024-01-01")).is_err()); // After max
197+
}
198+
199+
#[test]
200+
fn test_date_validator_format_with_constraints() {
201+
let mut params = HashMap::new();
202+
params.insert("format".into(), json!("YYYY-MM-DD"));
203+
params.insert("min".into(), json!("2023-01-01"));
204+
params.insert("max".into(), json!("2023-12-31"));
205+
let directive = create_directive(params);
206+
let validator = DateValidator::from_directive(directive).unwrap();
207+
208+
assert!(validator.validate(&json!("2023-01-01")).is_ok());
209+
assert!(validator.validate(&json!("2023-12-31")).is_ok());
210+
assert!(validator.validate(&json!("2023-06-15")).is_ok());
211+
assert!(validator.validate(&json!("2022-12-31")).is_err());
212+
assert!(validator.validate(&json!("2024-01-01")).is_err());
213+
}
214+
215+
#[test]
216+
fn test_date_validator_different_formats() {
217+
// Test YY-M-D format
218+
let mut params = HashMap::new();
219+
params.insert("format".into(), json!("YY-M-D"));
220+
let directive = create_directive(params);
221+
let validator = DateValidator::from_directive(directive).unwrap();
222+
223+
assert!(validator.validate(&json!("23-5-15")).is_ok());
224+
assert!(validator.validate(&json!("23-05-15")).is_ok());
225+
assert!(validator.validate(&json!("2023-5-15")).is_err());
226+
227+
// Test DD.MM.YYYY format
228+
let mut params = HashMap::new();
229+
params.insert("format".into(), json!("DD.MM.YYYY"));
230+
let directive = create_directive(params);
231+
let validator = DateValidator::from_directive(directive).unwrap();
232+
233+
assert!(validator.validate(&json!("15.05.2023")).is_ok());
234+
assert!(validator.validate(&json!("05.15.2023")).is_err()); // Invalid month
235+
}
236+
237+
#[test]
238+
fn test_invalid_directive_params() {
239+
// Test with invalid min date
240+
let mut params = HashMap::new();
241+
params.insert("format".into(), json!("YYYY-MM-DD"));
242+
params.insert("min".into(), json!("invalid-date"));
243+
let directive = create_directive(params);
244+
assert!(DateValidator::from_directive(directive).is_err());
245+
246+
// Test with invalid max date
247+
let mut params = HashMap::new();
248+
params.insert("format".into(), json!("YYYY-MM-DD"));
249+
params.insert("max".into(), json!("invalid-date"));
250+
let directive = create_directive(params);
251+
assert!(DateValidator::from_directive(directive).is_err());
252+
}
253+
254+
#[test]
255+
fn test_validator_name() {
256+
let params = HashMap::new();
257+
let directive = create_directive(params);
258+
let validator = DateValidator::from_directive(directive).unwrap();
259+
260+
assert_eq!(validator.name(), "@date");
261+
}
262+
263+
#[test]
264+
fn test_downcast_ref() {
265+
let params = HashMap::new();
266+
let directive = create_directive(params);
267+
let validator: Box<dyn Validator> =
268+
Box::new(DateValidator::from_directive(directive).unwrap());
269+
270+
let downcast = validator.downcast_ref::<DateValidator>();
271+
assert!(downcast.is_some());
272+
}
273+
}

aiscript-directive/src/validator/mod.rs

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

3+
use date::DateValidator;
34
use serde_json::Value;
45

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

8+
mod date;
9+
710
pub trait Validator: Send + Sync + Any {
811
fn name(&self) -> &'static str;
912
fn validate(&self, value: &Value) -> Result<(), String>;
@@ -258,6 +261,7 @@ impl FromDirective for Box<dyn Validator> {
258261
"in" => Ok(Box::new(InValidator::from_directive(directive)?)),
259262
"any" => Ok(Box::new(AnyValidator::from_directive(directive)?)),
260263
"not" => Ok(Box::new(NotValidator::from_directive(directive)?)),
264+
"date" => Ok(Box::new(DateValidator::from_directive(directive)?)),
261265
v => Err(format!("Invalid validators: @{}", v)),
262266
}
263267
}

0 commit comments

Comments
 (0)