web_url/
path.rs

1use std::fmt::{Display, Formatter};
2
3use crate::parse::Error;
4use crate::parse::Error::*;
5
6/// A web-based URL path.
7///
8/// # Validation
9/// A path will never be empty and will always start with a '/'.
10///
11/// The path string can contain any US-ASCII letter, number, or punctuation char excluding '?', and
12/// '#' since these chars denote the end of the path in the URL.
13#[derive(Copy, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Debug)]
14pub struct Path<'a> {
15    path: &'a str,
16}
17
18impl Default for Path<'static> {
19    fn default() -> Self {
20        Self { path: "/" }
21    }
22}
23
24impl<'a> Path<'a> {
25    //! Construction
26
27    /// Creates a new path.
28    ///
29    /// # Safety
30    /// The `path` must be valid.
31    pub unsafe fn new(path: &'a str) -> Self {
32        debug_assert!(Self::is_valid(path));
33
34        Self { path }
35    }
36}
37
38impl<'a> TryFrom<&'a str> for Path<'a> {
39    type Error = Error;
40
41    fn try_from(path: &'a str) -> Result<Self, Self::Error> {
42        if Self::is_valid(path) {
43            Ok(Self { path })
44        } else {
45            Err(InvalidPath)
46        }
47    }
48}
49
50impl<'a> Path<'a> {
51    //! Validation
52
53    /// Checks if the char `c` is valid.
54    fn is_valid_char(c: u8) -> bool {
55        c.is_ascii_alphanumeric() || (c.is_ascii_punctuation() && c != b'?' && c != b'#')
56    }
57
58    /// Checks if the `path` is valid.
59    pub fn is_valid(path: &str) -> bool {
60        !path.is_empty()
61            && path.as_bytes()[0] == b'/'
62            && path.as_bytes()[1..].iter().all(|c| Self::is_valid_char(*c))
63    }
64}
65
66impl<'a> Path<'a> {
67    //! Display
68
69    /// Gets the path string.
70    pub const fn as_str(&self) -> &str {
71        self.path
72    }
73}
74
75impl<'a> AsRef<str> for Path<'a> {
76    fn as_ref(&self) -> &str {
77        self.path
78    }
79}
80
81impl<'a> Display for Path<'a> {
82    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
83        write!(f, "{}", self.path)
84    }
85}
86
87impl<'a> Path<'a> {
88    //! Segments
89
90    /// Creates a new iterator for the path segments.
91    ///
92    /// # Example
93    /// `"/a/b/c/"` -> `["a", "b", "c", ""]`
94    pub const fn iter_segments(&self) -> impl Iterator<Item = &'a str> {
95        SegmentIterator {
96            remaining: self.path,
97        }
98    }
99}
100
101struct SegmentIterator<'a> {
102    remaining: &'a str,
103}
104
105impl<'a> Iterator for SegmentIterator<'a> {
106    type Item = &'a str;
107
108    fn next(&mut self) -> Option<Self::Item> {
109        if self.remaining.is_empty() {
110            None
111        } else {
112            self.remaining = &self.remaining[1..];
113            if let Some(slash) = self.remaining.as_bytes().iter().position(|c| *c == b'/') {
114                let segment: &str = &self.remaining[..slash];
115                self.remaining = &self.remaining[slash..];
116                Some(segment)
117            } else {
118                let segment: &str = self.remaining;
119                self.remaining = "";
120                Some(segment)
121            }
122        }
123    }
124}
125
126#[cfg(test)]
127mod tests {
128    use crate::Path;
129
130    #[test]
131    fn new() {
132        let path: Path = unsafe { Path::new("/the/path") };
133        assert_eq!(path.path, "/the/path");
134    }
135
136    #[test]
137    fn is_valid() {
138        let test_cases: &[(&str, bool)] = &[
139            ("", false),
140            ("/", true),
141            ("///", true),
142            ("/azAZ09", true),
143            ("/!/&/=/~/", true),
144            ("/?", false),
145            ("/#", false),
146        ];
147        for (path, expected) in test_cases {
148            let result: bool = Path::is_valid(path);
149            assert_eq!(result, *expected, "path={}", path);
150        }
151    }
152
153    #[test]
154    fn display() {
155        let path: Path = unsafe { Path::new("/the/path") };
156        assert_eq!(path.as_str(), "/the/path");
157        assert_eq!(path.as_ref(), "/the/path");
158        assert_eq!(path.to_string(), "/the/path");
159    }
160
161    #[test]
162    fn iter_segments() {
163        let path: Path = unsafe { Path::new("/") };
164        let result: Vec<&str> = path.iter_segments().collect();
165        let expected: Vec<&str> = vec![""];
166        assert_eq!(result, expected);
167
168        let path: Path = unsafe { Path::new("/the/path") };
169        let result: Vec<&str> = path.iter_segments().collect();
170        let expected: Vec<&str> = vec!["the", "path"];
171        assert_eq!(result, expected);
172
173        let path: Path = unsafe { Path::new("/the/path/") };
174        let result: Vec<&str> = path.iter_segments().collect();
175        let expected: Vec<&str> = vec!["the", "path", ""];
176        assert_eq!(result, expected)
177    }
178}