web_url/
scheme.rs

1use std::fmt::{Display, Formatter};
2
3/// A web-based URL scheme.
4///
5/// # RFC 3986
6/// https://datatracker.ietf.org/doc/html/rfc3986#section-3.1
7#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
8pub struct Scheme<'a> {
9    scheme: &'a str,
10}
11
12impl<'a> Scheme<'a> {
13    //! Construction
14
15    /// Creates a new scheme.
16    ///
17    /// # Safety
18    /// The `scheme` must be valid.
19    pub unsafe fn new(scheme: &'a str) -> Self {
20        debug_assert!(Self::is_valid(scheme, false));
21
22        Self { scheme }
23    }
24}
25
26impl<'a> Scheme<'a> {
27    //! Validation
28
29    /// Checks if the char `c` is a valid first char.
30    fn is_valid_first_char(c: u8, ignore_case: bool) -> bool {
31        c.is_ascii_lowercase() || (ignore_case && c.is_ascii_uppercase())
32    }
33
34    /// Checks if the char `c` is a valid scheme char.
35    fn is_valid_char(c: u8, ignore_case: bool) -> bool {
36        Self::is_valid_first_char(c, ignore_case)
37            || c.is_ascii_digit()
38            || c == b'+'
39            || c == b'-'
40            || c == b'.'
41    }
42
43    /// Checks if the `scheme` is valid.
44    pub fn is_valid(scheme: &'a str, ignore_case: bool) -> bool {
45        !scheme.is_empty()
46            && Self::is_valid_first_char(scheme.as_bytes()[0], ignore_case)
47            && scheme.as_bytes()[1..]
48                .iter()
49                .all(|c| Self::is_valid_char(*c, ignore_case))
50    }
51}
52
53impl<'a> Scheme<'a> {
54    //! Display
55
56    /// Gets the scheme string.
57    pub const fn as_str(&self) -> &str {
58        self.scheme
59    }
60}
61
62impl<'a> AsRef<str> for Scheme<'a> {
63    fn as_ref(&self) -> &str {
64        self.scheme
65    }
66}
67
68impl<'a> Display for Scheme<'a> {
69    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
70        write!(f, "{}", self.scheme)
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use crate::Scheme;
77
78    #[test]
79    fn new() {
80        let scheme: Scheme = unsafe { Scheme::new("scheme") };
81        assert_eq!(scheme.scheme, "scheme");
82    }
83
84    #[test]
85    fn is_valid() {
86        let test_cases: &[(&str, bool, bool)] = &[
87            ("", false, false),
88            ("A", true, false),
89            ("a", true, true),
90            ("0", false, false),
91            ("a~", false, false),
92            ("az09+-.", true, true),
93            ("azAZ09+-.", true, false),
94        ];
95        for (scheme, expected_ic_true, expected_ic_false) in test_cases {
96            let result: bool = Scheme::is_valid(scheme, true);
97            assert_eq!(result, *expected_ic_true, "scheme={}", scheme);
98
99            let result: bool = Scheme::is_valid(scheme, false);
100            assert_eq!(result, *expected_ic_false, "scheme={}", scheme);
101        }
102    }
103
104    #[test]
105    fn display() {
106        let scheme: Scheme = unsafe { Scheme::new("scheme") };
107        assert_eq!(scheme.as_str(), "scheme");
108        assert_eq!(scheme.as_ref(), "scheme");
109        assert_eq!(scheme.to_string(), "scheme");
110    }
111}