aiscript_directive/validator/
format.rs1use chrono::Datelike;
2use regex::Regex;
3use serde_json::Value;
4use std::any::Any;
5use std::sync::LazyLock;
6
7use crate::{Directive, DirectiveParams, FromDirective};
8
9use super::Validator;
10
11static EMAIL_REGEX: LazyLock<Regex> =
12 LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$").unwrap());
13
14static URL_REGEX: LazyLock<Regex> =
15 LazyLock::new(|| Regex::new(r"^https?://[\w.-]+(:\d+)?(/[\w/.~:%-]+)*/?(\?\S*)?$").unwrap());
16
17static UUID_REGEX: LazyLock<Regex> = LazyLock::new(|| {
18 Regex::new(r"^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap()
19});
20
21static IPV4_REGEX: LazyLock<Regex> = LazyLock::new(|| {
22 Regex::new(
23 r"^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$",
24 )
25 .unwrap()
26});
27
28static IPV6_REGEX: LazyLock<Regex> = LazyLock::new(|| {
29 Regex::new(r"^(([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]+|::(ffff(:0{1,4})?:)?((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1?[0-9])?[0-9])\.){3}(25[0-5]|(2[0-4]|1?[0-9])?[0-9]))$").unwrap()
30});
31
32static DATE_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d{4}-\d{2}-\d{2}$").unwrap());
33
34static DATETIME_REGEX: LazyLock<Regex> = LazyLock::new(|| {
35 Regex::new(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d+)?(Z|[+-]\d{2}:\d{2})?$").unwrap()
36});
37
38static TIME_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d{2}:\d{2}:\d{2}$").unwrap());
39
40static MONTH_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d{4}-\d{2}$").unwrap());
41
42static WEEK_REGEX: LazyLock<Regex> = LazyLock::new(|| Regex::new(r"^\d{4}-W\d{2}$").unwrap());
43
44static COLOR_REGEX: LazyLock<Regex> =
45 LazyLock::new(|| Regex::new(r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{6})$").unwrap());
46
47pub struct FormatValidator {
48 pub format_type: String,
49}
50
51impl Validator for FormatValidator {
53 fn name(&self) -> &'static str {
54 "@format"
55 }
56
57 fn validate(&self, value: &Value) -> Result<(), String> {
58 let value_str = match value.as_str() {
59 Some(s) => s,
60 None => return Err("Value must be a string".into()),
61 };
62
63 match self.format_type.as_str() {
64 "time" => {
66 if !TIME_REGEX.is_match(value_str) {
67 return Err("Value doesn't match time format (HH:MM:SS)".into());
68 }
69
70 let parts: Vec<&str> = value_str.split(':').collect();
72 if parts.len() != 3 {
73 return Err("Time must have hours, minutes, and seconds".into());
74 }
75
76 let hours: u32 = parts[0].parse().map_err(|_| "Invalid hours")?;
77 let minutes: u32 = parts[1].parse().map_err(|_| "Invalid minutes")?;
78 let seconds: u32 = parts[2].parse().map_err(|_| "Invalid seconds")?;
79
80 if hours >= 24 || minutes >= 60 || seconds >= 60 {
81 return Err("Invalid time components".into());
82 }
83
84 Ok(())
85 }
86 "month" => {
87 if !MONTH_REGEX.is_match(value_str) {
88 return Err("Value doesn't match month format (YYYY-MM)".into());
89 }
90
91 let parts: Vec<&str> = value_str.split('-').collect();
93 if parts.len() != 2 {
94 return Err("Month must have year and month parts".into());
95 }
96
97 let month: u32 = parts[1].parse().map_err(|_| "Invalid month")?;
98
99 if !(1..=12).contains(&month) {
100 return Err("Month must be between 1 and 12".into());
101 }
102
103 Ok(())
104 }
105 "week" => {
106 if !WEEK_REGEX.is_match(value_str) {
107 return Err("Value doesn't match week format (YYYY-Www)".into());
108 }
109
110 let year_part = &value_str[0..4];
112 let week_part = &value_str[6..8];
113
114 let year: i32 = year_part.parse().map_err(|_| "Invalid year")?;
115 let week: u32 = week_part.parse().map_err(|_| "Invalid week")?;
116
117 if !(1..=52).contains(&week) {
120 if week == 53 {
123 let has_week_53 = chrono::NaiveDate::from_ymd_opt(year, 1, 1)
125 .map(|date| {
126 let weekday = date.weekday().num_days_from_monday();
127 weekday == 3
128 || (weekday == 2
129 && chrono::NaiveDate::from_ymd_opt(year, 2, 29).is_some())
130 })
131 .unwrap_or(false);
132
133 if !has_week_53 {
134 return Err("This year doesn't have a week 53".into());
135 }
136 } else {
137 return Err(
138 "Week must be between 1 and 52 (or 53 for certain years)".into()
139 );
140 }
141 }
142
143 Ok(())
144 }
145 _ => {
147 let valid = match self.format_type.as_str() {
149 "email" => EMAIL_REGEX.is_match(value_str),
150 "url" => URL_REGEX.is_match(value_str),
151 "uuid" => UUID_REGEX.is_match(value_str),
152 "ipv4" => IPV4_REGEX.is_match(value_str),
153 "ipv6" => IPV6_REGEX.is_match(value_str),
154 "date" => {
155 if DATE_REGEX.is_match(value_str) {
156 if let Ok(date) =
157 chrono::NaiveDate::parse_from_str(value_str, "%Y-%m-%d")
158 {
159 let year = date.year();
160 let month = date.month();
161 let day = date.day();
162 year >= 1 && (1..=12).contains(&month) && (1..=31).contains(&day)
163 } else {
164 false
165 }
166 } else {
167 false
168 }
169 }
170 "datetime" => {
171 DATETIME_REGEX.is_match(value_str)
172 && chrono::DateTime::parse_from_rfc3339(value_str).is_ok()
173 }
174 "color" => COLOR_REGEX.is_match(value_str),
175 _ => return Err(format!("Unsupported format type: {}", self.format_type)),
176 };
177
178 if valid {
179 Ok(())
180 } else {
181 Err(format!("Value doesn't match {} format", self.format_type))
182 }
183 }
184 }
185 }
186
187 fn as_any(&self) -> &dyn Any {
188 self
189 }
190}
191impl FromDirective for FormatValidator {
193 fn from_directive(directive: Directive) -> Result<Self, String> {
194 match directive.params {
196 DirectiveParams::KeyValue(params) => {
197 match params.get("type").and_then(|v| v.as_str()) {
198 Some(format_type) => match format_type {
199 "email" | "url" | "uuid" | "ipv4" | "ipv6" | "date" | "datetime"
200 | "time" | "month" | "week" | "color" => Ok(Self {
201 format_type: format_type.to_string(),
202 }),
203 _ => Err(format!("Unsupported format type: {}", format_type)),
204 },
205 None => Err("@format directive requires a 'type' parameter".into()),
206 }
207 }
208 _ => Err("Invalid params for @format directive".into()),
209 }
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use crate::{Directive, DirectiveParams};
217 use serde_json::json;
218 use std::collections::HashMap;
219
220 fn create_directive(params: HashMap<String, Value>) -> Directive {
221 Directive {
222 name: "format".into(),
223 params: DirectiveParams::KeyValue(params),
224 line: 1,
225 }
226 }
227
228 #[test]
229 fn test_email_format() {
230 let mut params = HashMap::new();
231 params.insert("type".into(), json!("email"));
232 let directive = create_directive(params);
233 let validator = FormatValidator::from_directive(directive).unwrap();
234
235 assert!(validator.validate(&json!("user@example.com")).is_ok());
236 assert!(
237 validator
238 .validate(&json!("user.name+tag@example.co.uk"))
239 .is_ok()
240 );
241 assert!(validator.validate(&json!("invalid-email")).is_err());
242 assert!(validator.validate(&json!("missing@domain")).is_err());
243 assert!(validator.validate(&json!("@example.com")).is_err());
244 }
245
246 #[test]
247 fn test_url_format() {
248 let mut params = HashMap::new();
249 params.insert("type".into(), json!("url"));
250 let directive = create_directive(params);
251 let validator = FormatValidator::from_directive(directive).unwrap();
252
253 assert!(validator.validate(&json!("http://example.com")).is_ok());
254 assert!(
255 validator
256 .validate(&json!("https://subdomain.example.com/path"))
257 .is_ok()
258 );
259 assert!(
260 validator
261 .validate(&json!("https://example.com/path?query=value"))
262 .is_ok()
263 );
264 assert!(validator.validate(&json!("example.com")).is_err());
265 assert!(validator.validate(&json!("http://")).is_err());
266 }
267
268 #[test]
269 fn test_uuid_format() {
270 let mut params = HashMap::new();
271 params.insert("type".into(), json!("uuid"));
272 let directive = create_directive(params);
273 let validator = FormatValidator::from_directive(directive).unwrap();
274
275 assert!(
276 validator
277 .validate(&json!("123e4567-e89b-12d3-a456-426614174000"))
278 .is_ok()
279 );
280 assert!(
281 validator
282 .validate(&json!("123e4567-e89b-12d3-a456-42661417400"))
283 .is_err()
284 ); assert!(
286 validator
287 .validate(&json!("123e4567-e89b-12d3-a456-4266141740000"))
288 .is_err()
289 ); assert!(
291 validator
292 .validate(&json!("123e4567e89b12d3a456426614174000"))
293 .is_err()
294 ); }
296
297 #[test]
298 fn test_ipv4_format() {
299 let mut params = HashMap::new();
300 params.insert("type".into(), json!("ipv4"));
301 let directive = create_directive(params);
302 let validator = FormatValidator::from_directive(directive).unwrap();
303
304 assert!(validator.validate(&json!("192.168.0.1")).is_ok());
305 assert!(validator.validate(&json!("127.0.0.1")).is_ok());
306 assert!(validator.validate(&json!("255.255.255.255")).is_ok());
307 assert!(validator.validate(&json!("256.0.0.1")).is_err()); assert!(validator.validate(&json!("192.168.0")).is_err()); assert!(validator.validate(&json!("192.168.0.1.5")).is_err()); }
311
312 #[test]
313 fn test_ipv6_format() {
314 let mut params = HashMap::new();
315 params.insert("type".into(), json!("ipv6"));
316 let directive = create_directive(params);
317 let validator = FormatValidator::from_directive(directive).unwrap();
318
319 assert!(
320 validator
321 .validate(&json!("2001:0db8:85a3:0000:0000:8a2e:0370:7334"))
322 .is_ok()
323 );
324 assert!(validator.validate(&json!("::1")).is_ok()); assert!(validator.validate(&json!("2001:db8::")).is_ok()); assert!(validator.validate(&json!("192.168.0.1")).is_err()); assert!(
328 validator
329 .validate(&json!("2001:db8:85a3:0000:0000:8a2e:0370:7334:1234"))
330 .is_err()
331 ); }
333
334 #[test]
335 fn test_date_format() {
336 let mut params = HashMap::new();
337 params.insert("type".into(), json!("date"));
338 let directive = create_directive(params);
339 let validator = FormatValidator::from_directive(directive).unwrap();
340
341 assert!(validator.validate(&json!("2023-01-15")).is_ok());
342 assert!(validator.validate(&json!("2023-02-28")).is_ok());
343 assert!(validator.validate(&json!("2023-02-30")).is_err()); assert!(validator.validate(&json!("2023-13-01")).is_err()); assert!(validator.validate(&json!("01-15-2023")).is_err()); assert!(validator.validate(&json!("2023/01/15")).is_err()); }
348
349 #[test]
350 fn test_datetime_format() {
351 let mut params = HashMap::new();
352 params.insert("type".into(), json!("datetime"));
353 let directive = create_directive(params);
354 let validator = FormatValidator::from_directive(directive).unwrap();
355
356 assert!(validator.validate(&json!("2023-01-15T12:30:45Z")).is_ok());
357 assert!(
358 validator
359 .validate(&json!("2023-01-15T12:30:45+01:00"))
360 .is_ok()
361 );
362 assert!(
363 validator
364 .validate(&json!("2023-01-15T12:30:45.123Z"))
365 .is_ok()
366 );
367 assert!(validator.validate(&json!("2023-01-15 12:30:45")).is_err()); assert!(validator.validate(&json!("2023-01-15T25:30:45Z")).is_err()); }
370
371 #[test]
372 fn test_time_format() {
373 let mut params = HashMap::new();
374 params.insert("type".into(), json!("time"));
375 let directive = create_directive(params);
376 let validator = FormatValidator::from_directive(directive).unwrap();
377
378 assert!(validator.validate(&json!("12:30:45")).is_ok());
379 assert!(validator.validate(&json!("00:00:00")).is_ok());
380 assert!(validator.validate(&json!("23:59:59")).is_ok());
381 assert!(validator.validate(&json!("24:00:00")).is_err()); assert!(validator.validate(&json!("12:60:45")).is_err()); assert!(validator.validate(&json!("12:30")).is_err()); }
385
386 #[test]
387 fn test_month_format() {
388 let mut params = HashMap::new();
389 params.insert("type".into(), json!("month"));
390 let directive = create_directive(params);
391 let validator = FormatValidator::from_directive(directive).unwrap();
392
393 assert!(validator.validate(&json!("2023-01")).is_ok());
394 assert!(validator.validate(&json!("2023-12")).is_ok());
395 assert!(validator.validate(&json!("2023-13")).is_err()); assert!(validator.validate(&json!("01-2023")).is_err()); }
398
399 #[test]
400 fn test_week_format() {
401 let mut params = HashMap::new();
402 params.insert("type".into(), json!("week"));
403 let directive = create_directive(params);
404 let validator = FormatValidator::from_directive(directive).unwrap();
405
406 assert!(validator.validate(&json!("2023-W01")).is_ok());
407 assert!(validator.validate(&json!("2023-W52")).is_ok());
408 assert!(validator.validate(&json!("2023-W00")).is_err()); assert!(validator.validate(&json!("2023-W53")).is_err()); assert!(validator.validate(&json!("2023W01")).is_err()); }
412
413 #[test]
414 fn test_color_format() {
415 let mut params = HashMap::new();
416 params.insert("type".into(), json!("color"));
417 let directive = create_directive(params);
418 let validator = FormatValidator::from_directive(directive).unwrap();
419
420 assert!(validator.validate(&json!("#000000")).is_ok());
421 assert!(validator.validate(&json!("#FFFFFF")).is_ok());
422 assert!(validator.validate(&json!("#123")).is_ok());
423 assert!(validator.validate(&json!("#1234")).is_err()); assert!(validator.validate(&json!("000000")).is_err()); assert!(validator.validate(&json!("#GHIJKL")).is_err()); }
427
428 #[test]
429 fn test_invalid_format_type() {
430 let mut params = HashMap::new();
431 params.insert("type".into(), json!("invalid_type"));
432 let directive = create_directive(params);
433 assert!(FormatValidator::from_directive(directive).is_err());
434 }
435
436 #[test]
437 fn test_missing_type_parameter() {
438 let params = HashMap::new();
439 let directive = create_directive(params);
440 assert!(FormatValidator::from_directive(directive).is_err());
441 }
442
443 #[test]
444 fn test_non_string_value() {
445 let mut params = HashMap::new();
446 params.insert("type".into(), json!("email"));
447 let directive = create_directive(params);
448 let validator = FormatValidator::from_directive(directive).unwrap();
449
450 assert!(validator.validate(&json!(123)).is_err());
451 assert!(validator.validate(&json!(true)).is_err());
452 assert!(validator.validate(&json!(null)).is_err());
453 assert!(validator.validate(&json!(["email@example.com"])).is_err());
454 }
455}