aiscript_directive/validator/
format.rs

1use 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
51// Improve the validate method for these formats
52impl 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            // Other formats remain unchanged
65            "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                // Validate time components
71                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                // Validate month components
92                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                // Validate week components
111                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                // ISO 8601 defines the valid range for week numbers is 1-53,
118                // but only certain years have a 53rd week
119                if !(1..=52).contains(&week) {
120                    // Week 53 is only valid in years where January 1 is a Thursday
121                    // or in leap years where January 1 is a Wednesday
122                    if week == 53 {
123                        // Calculate if this year has 53 weeks
124                        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            // Handle other formats here as before
146            _ => {
147                // Original validation for other formats...
148                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}
191// The FromDirective implementation remains the same
192impl FromDirective for FormatValidator {
193    fn from_directive(directive: Directive) -> Result<Self, String> {
194        // Same implementation as before
195        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        ); // too short
285        assert!(
286            validator
287                .validate(&json!("123e4567-e89b-12d3-a456-4266141740000"))
288                .is_err()
289        ); // too long
290        assert!(
291            validator
292                .validate(&json!("123e4567e89b12d3a456426614174000"))
293                .is_err()
294        ); // no hyphens
295    }
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()); // out of range
308        assert!(validator.validate(&json!("192.168.0")).is_err()); // too few octets
309        assert!(validator.validate(&json!("192.168.0.1.5")).is_err()); // too many octets
310    }
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()); // localhost
325        assert!(validator.validate(&json!("2001:db8::")).is_ok()); // with ::
326        assert!(validator.validate(&json!("192.168.0.1")).is_err()); // IPv4
327        assert!(
328            validator
329                .validate(&json!("2001:db8:85a3:0000:0000:8a2e:0370:7334:1234"))
330                .is_err()
331        ); // too long
332    }
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()); // invalid day
344        assert!(validator.validate(&json!("2023-13-01")).is_err()); // invalid month
345        assert!(validator.validate(&json!("01-15-2023")).is_err()); // wrong format
346        assert!(validator.validate(&json!("2023/01/15")).is_err()); // wrong separator
347    }
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()); // missing T
368        assert!(validator.validate(&json!("2023-01-15T25:30:45Z")).is_err()); // invalid hour
369    }
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()); // invalid hour
382        assert!(validator.validate(&json!("12:60:45")).is_err()); // invalid minute
383        assert!(validator.validate(&json!("12:30")).is_err()); // missing seconds
384    }
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()); // invalid month
396        assert!(validator.validate(&json!("01-2023")).is_err()); // wrong format
397    }
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()); // invalid week
409        assert!(validator.validate(&json!("2023-W53")).is_err()); // invalid week
410        assert!(validator.validate(&json!("2023W01")).is_err()); // missing hyphen
411    }
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()); // invalid length
424        assert!(validator.validate(&json!("000000")).is_err()); // missing #
425        assert!(validator.validate(&json!("#GHIJKL")).is_err()); // invalid hex
426    }
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}