Skip to main content

data_protocol_validator/
format.rs

1use regex::Regex;
2
3/// Validate a string value against a named format.
4///
5/// Returns `true` if the value satisfies the format, or if the format is
6/// unknown (unknown formats are treated as always valid, matching JSON Schema
7/// behaviour).
8pub fn validate_format(value: &str, format: &str) -> bool {
9    match format {
10        "date" => validate_date(value),
11        "date-time" => validate_datetime(value),
12        "email" => validate_email(value),
13        "uri" => validate_uri(value),
14        "uuid" => validate_uuid(value),
15        _ => true, // unknown formats pass
16    }
17}
18
19fn validate_date(value: &str) -> bool {
20    let re = Regex::new(r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])$").unwrap();
21    if !re.is_match(value) {
22        return false;
23    }
24    // Also check that the date is actually parseable
25    chrono::NaiveDate::parse_from_str(value, "%Y-%m-%d").is_ok()
26}
27
28fn validate_datetime(value: &str) -> bool {
29    let re = Regex::new(
30        r"^\d{4}-(?:0[1-9]|1[0-2])-(?:0[1-9]|[12]\d|3[01])T(?:[01]\d|2[0-3]):[0-5]\d:[0-5]\d(?:\.\d+)?(?:Z|[+-](?:[01]\d|2[0-3]):[0-5]\d)$"
31    ).unwrap();
32    if !re.is_match(value) {
33        return false;
34    }
35    // Verify the date-time is parseable
36    use chrono::{DateTime, FixedOffset};
37    DateTime::<FixedOffset>::parse_from_rfc3339(value).is_ok()
38        || chrono::NaiveDateTime::parse_from_str(value, "%Y-%m-%dT%H:%M:%S%.fZ").is_ok()
39}
40
41fn validate_email(value: &str) -> bool {
42    let re = Regex::new(r"^[^\s@]+@[^\s@]+\.[^\s@]+$").unwrap();
43    re.is_match(value)
44}
45
46fn validate_uri(value: &str) -> bool {
47    let re = Regex::new(r"^[a-zA-Z][a-zA-Z0-9+\-.]*://[^\s]+$").unwrap();
48    re.is_match(value)
49}
50
51fn validate_uuid(value: &str) -> bool {
52    let re =
53        Regex::new(r"(?i)^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$").unwrap();
54    re.is_match(value)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn test_date_valid() {
63        assert!(validate_format("2024-01-15", "date"));
64    }
65
66    #[test]
67    fn test_date_invalid_month() {
68        assert!(!validate_format("2024-13-01", "date"));
69    }
70
71    #[test]
72    fn test_datetime_valid() {
73        assert!(validate_format("2024-01-15T10:30:00Z", "date-time"));
74    }
75
76    #[test]
77    fn test_datetime_with_offset() {
78        assert!(validate_format("2024-01-15T10:30:00+09:00", "date-time"));
79    }
80
81    #[test]
82    fn test_email_valid() {
83        assert!(validate_format("user@example.com", "email"));
84    }
85
86    #[test]
87    fn test_email_invalid() {
88        assert!(!validate_format("not-an-email", "email"));
89    }
90
91    #[test]
92    fn test_uri_valid() {
93        assert!(validate_format("https://example.com/path", "uri"));
94    }
95
96    #[test]
97    fn test_uuid_valid() {
98        assert!(validate_format(
99            "550e8400-e29b-41d4-a716-446655440000",
100            "uuid"
101        ));
102    }
103
104    #[test]
105    fn test_unknown_format_passes() {
106        assert!(validate_format("anything", "unknown-format"));
107    }
108}