icydb_model/base/normalizer/
web.rs1use crate::{prelude::*, visitor::Normalizer};
8
9#[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#[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#[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}