Skip to main content

web_url/
scheme.rs

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