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;