Skip to main content

web_url/
path.rs

1use std::fmt::{Display, Formatter};
2
3use crate::parse::Error;
4use crate::parse::Error::InvalidPath;
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::parse::Error::InvalidPath;
129    use crate::Path;
130
131    #[test]
132    fn new() {
133        let path: Path = unsafe { Path::new("/the/path") };
134        assert_eq!(path.path, "/the/path");
135    }
136
137    #[test]
138    fn default() {
139        let path: Path = Path::default();
140        assert_eq!(path.as_str(), "/");
141    }
142
143    #[test]
144    fn try_from_str() {
145        assert_eq!(Path::try_from("/").unwrap().as_str(), "/");
146        assert_eq!(Path::try_from("/the/path").unwrap().as_str(), "/the/path");
147        assert_eq!(Path::try_from(""), Err(InvalidPath));
148        assert_eq!(Path::try_from("no-slash"), Err(InvalidPath));
149        assert_eq!(Path::try_from("/?"), Err(InvalidPath));
150        assert_eq!(Path::try_from("/#"), Err(InvalidPath));
151    }
152
153    #[test]
154    fn is_valid() {
155        let test_cases: &[(&str, bool)] = &[
156            ("", false),
157            ("/", true),
158            ("///", true),
159            ("/azAZ09", true),
160            ("/!/&/=/~/", true),
161            ("/?", false),
162            ("/#", false),
163        ];
164        for (path, expected) in test_cases {
165            let result: bool = Path::is_valid(path);
166            assert_eq!(result, *expected, "path={}", path);
167        }
168    }
169
170    #[test]
171    fn display() {
172        let path: Path = unsafe { Path::new("/the/path") };
173        assert_eq!(path.as_str(), "/the/path");
174        assert_eq!(path.as_ref(), "/the/path");
175        assert_eq!(path.to_string(), "/the/path");
176    }
177
178    #[test]
179    fn iter_segments() {
180        let path: Path = unsafe { Path::new("/") };
181        let result: Vec<&str> = path.iter_segments().collect();
182        let expected: Vec<&str> = vec![""];
183        assert_eq!(result, expected);
184
185        let path: Path = unsafe { Path::new("/the/path") };
186        let result: Vec<&str> = path.iter_segments().collect();
187        let expected: Vec<&str> = vec!["the", "path"];
188        assert_eq!(result, expected);
189
190        let path: Path = unsafe { Path::new("/the/path/") };
191        let result: Vec<&str> = path.iter_segments().collect();
192        let expected: Vec<&str> = vec!["the", "path", ""];
193        assert_eq!(result, expected)
194    }
195}