Skip to main content

web_url/url/
web_url.rs

1use address::IPAddress;
2
3/// A web-based URL.
4///
5/// # Format
6/// All web-based URLs will be in the format: `scheme://host:port/path?query#fragment`.
7/// This is a sub-set of [RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986#section-4.3).
8///
9/// The port, query, and fragment are all optional.
10/// The path will never be empty and will always start with a '/'.
11#[derive(Clone, Debug)]
12pub struct WebUrl {
13    pub(in crate::url) url: String,
14    pub(in crate::url) scheme_len: u32,
15    pub(in crate::url) host_end: u32,
16    pub(in crate::url) ip: Option<IPAddress>,
17    pub(in crate::url) port_end: u32,
18    pub(in crate::url) port: Option<u16>,
19    pub(in crate::url) path_end: u32,
20    pub(in crate::url) query_end: u32,
21}
22
23impl WebUrl {
24    //! Construction
25
26    /// Creates a new web-based URL.
27    ///
28    /// # Safety
29    /// The parameters must be valid.
30    #[allow(clippy::too_many_arguments)]
31    pub unsafe fn new<S>(
32        url: S,
33        scheme_len: u32,
34        host_end: u32,
35        ip: Option<IPAddress>,
36        port_end: u32,
37        port: Option<u16>,
38        path_end: u32,
39        query_end: u32,
40    ) -> Self
41    where
42        S: Into<String>,
43    {
44        Self {
45            url: url.into(),
46            scheme_len,
47            host_end,
48            ip,
49            port_end,
50            port,
51            path_end,
52            query_end,
53        }
54    }
55}
56
57impl WebUrl {
58    //! Properties
59
60    /// Gets the length.
61    pub fn len(&self) -> usize {
62        self.url.len()
63    }
64
65    /// Checks if the url is empty.
66    pub fn is_empty(&self) -> bool {
67        false
68    }
69}
70
71#[cfg(test)]
72mod tests {
73    use crate::parse::Error::*;
74    use crate::WebUrl;
75    use std::str::FromStr;
76
77    #[test]
78    fn len() {
79        let url = WebUrl::from_str("https://example.com/path").unwrap();
80        assert_eq!(url.len(), "https://example.com/path".len());
81    }
82
83    #[test]
84    fn is_empty() {
85        let url = WebUrl::from_str("https://example.com").unwrap();
86        assert!(!url.is_empty());
87    }
88
89    #[test]
90    fn parse_full_url() {
91        let url = WebUrl::from_str("https://example.com:8080/path?key=value#section").unwrap();
92        assert_eq!(url.scheme().as_str(), "https");
93        assert_eq!(url.port(), Some(8080));
94        assert_eq!(url.path().as_str(), "/path");
95        assert_eq!(url.query().unwrap().as_str(), "?key=value");
96        assert_eq!(url.fragment().unwrap().as_str(), "#section");
97    }
98
99    #[test]
100    fn parse_no_path_appends_slash() {
101        let url = WebUrl::from_str("https://example.com").unwrap();
102        assert_eq!(url.path().as_str(), "/");
103        assert_eq!(url.as_str(), "https://example.com/");
104    }
105
106    #[test]
107    fn parse_uppercase_lowercased() {
108        let url = WebUrl::from_str("HTTPS://EXAMPLE.COM/Path").unwrap();
109        assert_eq!(url.scheme().as_str(), "https");
110        assert_eq!(url.as_str(), "https://example.com/Path");
111    }
112
113    #[test]
114    fn parse_invalid_scheme() {
115        assert_eq!(WebUrl::from_str("").unwrap_err(), InvalidScheme);
116        assert_eq!(WebUrl::from_str("://host").unwrap_err(), InvalidScheme);
117        assert_eq!(WebUrl::from_str("0http://host").unwrap_err(), InvalidScheme);
118    }
119
120    #[test]
121    fn parse_invalid_host() {
122        assert_eq!(WebUrl::from_str("http://").unwrap_err(), InvalidHost);
123        assert_eq!(WebUrl::from_str("http://ho!st").unwrap_err(), InvalidHost);
124    }
125
126    #[test]
127    fn parse_invalid_port() {
128        assert_eq!(
129            WebUrl::from_str("http://host:bad").unwrap_err(),
130            InvalidPort
131        );
132        assert_eq!(
133            WebUrl::from_str("http://host:99999").unwrap_err(),
134            InvalidPort
135        );
136    }
137
138    #[test]
139    fn parse_invalid_path() {
140        assert_eq!(
141            WebUrl::from_str("http://host/path with space").unwrap_err(),
142            InvalidPath
143        );
144    }
145
146    #[test]
147    fn parse_invalid_query() {
148        assert_eq!(
149            WebUrl::from_str("http://host/path?query with space").unwrap_err(),
150            InvalidQuery
151        );
152    }
153
154    #[test]
155    fn parse_invalid_fragment() {
156        assert_eq!(
157            WebUrl::from_str("http://host/path# bad").unwrap_err(),
158            InvalidFragment
159        );
160    }
161
162    #[test]
163    fn try_from_string_ok() {
164        let input = String::from("https://example.com/path");
165        let url = WebUrl::try_from(input).unwrap();
166        assert_eq!(url.as_str(), "https://example.com/path");
167    }
168
169    #[test]
170    fn try_from_string_err_returns_string() {
171        let input = String::from("not a url");
172        let (error, returned) = WebUrl::try_from(input).unwrap_err();
173        assert_eq!(error, InvalidScheme);
174        assert_eq!(returned, "not a url");
175    }
176
177    #[test]
178    fn try_from_string_no_path() {
179        let input = String::from("https://example.com");
180        let url = WebUrl::try_from(input).unwrap();
181        assert_eq!(url.as_str(), "https://example.com/");
182    }
183}