Skip to main content

icydb_model/base/normalizer/
web.rs

1//! Module: base::normalizer::web
2//!
3//! Responsibility: base normalizer definitions.
4//! Does not own: validation policy, persistence, or schema mutation semantics.
5//! Boundary: mutates schema field values through facade normalizer traits.
6
7use crate::{prelude::*, visitor::Normalizer};
8
9///
10/// MimeType
11/// Lowercases and trims whitespace.
12///
13
14#[normalizer]
15pub struct MimeType;
16
17impl Normalizer<String> for MimeType {
18    fn normalize(&self, value: &mut String) -> Result<(), String> {
19        let trimmed = value.trim();
20
21        if trimmed.len() != value.len() {
22            *value = trimmed.to_owned();
23        }
24
25        value.make_ascii_lowercase();
26
27        Ok(())
28    }
29}
30
31///
32/// Url
33/// Trims whitespace and ensures a valid scheme (adds `https://` if missing).
34///
35
36#[normalizer]
37pub struct Url;
38
39impl Normalizer<String> for Url {
40    fn normalize(&self, value: &mut String) -> Result<(), String> {
41        let trimmed = value.trim();
42
43        let normalized = if trimmed.starts_with("http://") || trimmed.starts_with("https://") {
44            trimmed.to_owned()
45        } else if trimmed.contains("://") || has_explicit_non_numeric_colon(trimmed) {
46            return Err("URL scheme must be http or https".to_string());
47        } else {
48            format!("https://{trimmed}")
49        };
50
51        *value = normalized;
52
53        Ok(())
54    }
55}
56
57fn has_explicit_non_numeric_colon(value: &str) -> bool {
58    let boundary = value.find(['/', '?', '#']).unwrap_or(value.len());
59    let head = &value[..boundary];
60    let Some((_host, port_or_scheme)) = head.rsplit_once(':') else {
61        return false;
62    };
63
64    port_or_scheme.is_empty() || !port_or_scheme.chars().all(|ch| ch.is_ascii_digit())
65}
66
67///
68/// TESTS
69///
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_mime_type_normalize_trims_and_lowercases() {
77        let normalizer = MimeType;
78
79        let mut v = "  Text/HTML  ".to_string();
80        normalizer.normalize(&mut v).unwrap();
81        assert_eq!(v, "text/html");
82
83        let mut v = "APPLICATION/JSON".to_string();
84        normalizer.normalize(&mut v).unwrap();
85        assert_eq!(v, "application/json");
86
87        let mut v = " image/JPEG ".to_string();
88        normalizer.normalize(&mut v).unwrap();
89        assert_eq!(v, "image/jpeg");
90    }
91
92    #[test]
93    fn test_url_normalize_adds_https_when_missing() {
94        let normalizer = Url;
95
96        let mut v = "example.com".to_string();
97        normalizer.normalize(&mut v).unwrap();
98        assert_eq!(v, "https://example.com");
99
100        let mut v = " www.example.com ".to_string();
101        normalizer.normalize(&mut v).unwrap();
102        assert_eq!(v, "https://www.example.com");
103
104        let mut v = "example.com:8080/path".to_string();
105        normalizer.normalize(&mut v).unwrap();
106        assert_eq!(v, "https://example.com:8080/path");
107    }
108
109    #[test]
110    fn test_url_normalize_keeps_existing_scheme() {
111        let normalizer = Url;
112
113        let mut v = "https://example.com".to_string();
114        normalizer.normalize(&mut v).unwrap();
115        assert_eq!(v, "https://example.com");
116
117        let mut v = "http://example.com".to_string();
118        normalizer.normalize(&mut v).unwrap();
119        assert_eq!(v, "http://example.com");
120    }
121
122    #[test]
123    fn test_url_normalize_trims_whitespace() {
124        let normalizer = Url;
125
126        let mut v = "   https://example.com   ".to_string();
127        normalizer.normalize(&mut v).unwrap();
128        assert_eq!(v, "https://example.com");
129
130        let mut v = "   example.com   ".to_string();
131        normalizer.normalize(&mut v).unwrap();
132        assert_eq!(v, "https://example.com");
133    }
134
135    #[test]
136    fn test_url_normalize_rejects_explicit_unsupported_scheme() {
137        let normalizer = Url;
138
139        let mut v = "ftp://example.com".to_string();
140        assert!(normalizer.normalize(&mut v).is_err());
141
142        let mut v = "javascript:alert(1)".to_string();
143        assert!(normalizer.normalize(&mut v).is_err());
144    }
145}