Skip to main content

formars_core/
validators.rs

1/// Pragmatic email check: requires one `@` splitting non-empty local and
2/// domain parts; domain must have a dotted suffix with alphanumeric labels.
3/// NOT RFC 5322 — quoted locals, IP-literal domains and IDN are rejected.
4/// For stricter needs use `.refine(|s| ..)` with a dedicated crate.
5pub(crate) fn is_plausible_email(s: &str) -> bool {
6    let Some((local, domain)) = s.split_once('@') else {
7        return false;
8    };
9    if s.matches('@').count() != 1 || local.is_empty() || !domain.contains('.') {
10        return false;
11    }
12    if s.chars().any(char::is_whitespace) {
13        return false;
14    }
15    let labels: Vec<&str> = domain.split('.').collect();
16    labels.len() >= 2
17        && labels
18            .iter()
19            .all(|l| !l.is_empty() && l.chars().all(|c| c.is_ascii_alphanumeric() || c == '-'))
20}
21
22/// Pragmatic URL check: requires `scheme://` prefix, non-empty host, no
23/// whitespace. NOT RFC 3986 — percent-encoding validity is not verified.
24/// For stricter needs use `.refine(|s| ..)` with the `url` crate via refine.
25pub(crate) fn is_plausible_url(s: &str) -> bool {
26    let Some((scheme, rest)) = s.split_once("://") else {
27        return false;
28    };
29    if scheme.is_empty()
30        || !scheme
31            .chars()
32            .next()
33            .is_some_and(|c| c.is_ascii_alphabetic())
34    {
35        return false;
36    }
37    if !scheme
38        .chars()
39        .all(|c| c.is_ascii_alphanumeric() || c == '+' || c == '-' || c == '.')
40    {
41        return false;
42    }
43    let authority = rest.split(['/', '?', '#']).next().unwrap_or("");
44    let host = authority.split(':').next().unwrap_or(authority);
45    !host.is_empty() && !s.chars().any(char::is_whitespace)
46}
47
48/// Pragmatic UUID check: canonical 8-4-4-4-12 hex groups (case-insensitive).
49/// Braced/urn forms are rejected in v0.
50pub(crate) fn is_plausible_uuid(s: &str) -> bool {
51    let groups: Vec<&str> = s.split('-').collect();
52    if groups.len() != 5 || !groups.iter().map(|g| g.len()).eq([8usize, 4, 4, 4, 12]) {
53        return false;
54    }
55    groups
56        .iter()
57        .all(|g| g.chars().all(|c| c.is_ascii_hexdigit()))
58}
59
60#[cfg(test)]
61mod tests {
62    use crate::validators::{is_plausible_email, is_plausible_url, is_plausible_uuid};
63
64    #[test]
65    fn email_fixture_table() {
66        let accept = ["user@example.com", "a.b+tag@sub.domain.org", "x@y.co"];
67        let reject = [
68            "",
69            "not-an-email",
70            "@example.com",
71            "user@",
72            "user@@example.com",
73            "user@example..com",
74            "us er@example.com",
75            "user@exa mple.com",
76            "user@example.",
77            "user@ex\u{e4}mple.com",
78        ];
79        for s in accept {
80            assert!(is_plausible_email(s), "expected accept: {s}");
81        }
82        for s in reject {
83            assert!(!is_plausible_email(s), "expected reject: {s}");
84        }
85    }
86
87    #[test]
88    fn url_fixture_table() {
89        let accept = [
90            "https://example.com",
91            "http://example.com/path?query=1#frag",
92            "https://sub.domain.org:8443/x",
93            "ftp://files.example.org",
94        ];
95        let reject = [
96            "",
97            "example.com",
98            "https://",
99            "http:///path",
100            "://example.com",
101            "https://:8080/x",
102        ];
103        for s in accept {
104            assert!(is_plausible_url(s), "expected accept: {s}");
105        }
106        for s in reject {
107            assert!(!is_plausible_url(s), "expected reject: {s}");
108        }
109    }
110
111    #[test]
112    fn uuid_fixture_table() {
113        let accept = [
114            "123e4567-e89b-12d3-a456-426614174000",
115            "00000000-0000-0000-0000-000000000000",
116            "123E4567-E89B-12D3-A456-426614174000",
117        ];
118        let reject = [
119            "",
120            "123e4567e89b12d3a456426614174000",
121            "123e4567-e89b-12d3-a456-42661417400g",
122            "123e4567-e89b-12d3-a456",
123            "zzzzzzzz-zzzz-zzzz-zzzz-zzzzzzzzzzzz",
124        ];
125        for s in accept {
126            assert!(is_plausible_uuid(s), "expected accept: {s}");
127        }
128        for s in reject {
129            assert!(!is_plausible_uuid(s), "expected reject: {s}");
130        }
131    }
132}