1use core::{fmt, str::FromStr};
4
5use serde::{de::Error as SerdeError, Deserialize, Deserializer, Serialize, Serializer};
6
7use crate::{error::Error, prelude::*};
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
11pub enum Scheme {
12 Http,
13 Https,
14 WebSocket,
15 SecureWebSocket,
16}
17
18impl fmt::Display for Scheme {
19 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20 match self {
21 Scheme::Http => write!(f, "http"),
22 Scheme::Https => write!(f, "https"),
23 Scheme::WebSocket => write!(f, "ws"),
24 Scheme::SecureWebSocket => write!(f, "wss"),
25 }
26 }
27}
28
29impl FromStr for Scheme {
30 type Err = crate::Error;
31
32 fn from_str(s: &str) -> Result<Self, Self::Err> {
33 Ok(match s {
34 "http" | "tcp" => Scheme::Http,
35 "https" => Scheme::Https,
36 "ws" => Scheme::WebSocket,
37 "wss" => Scheme::SecureWebSocket,
38 _ => return Err(Error::unsupported_scheme(s.to_string())),
39 })
40 }
41}
42
43#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
49pub struct Url {
50 inner: url::Url,
51 scheme: Scheme,
52}
53
54impl FromStr for Url {
55 type Err = Error;
56
57 fn from_str(s: &str) -> Result<Self, Self::Err> {
58 let url: url::Url = s.parse().map_err(Error::parse_url)?;
59 url.try_into()
60 }
61}
62
63impl Url {
64 pub fn is_secure(&self) -> bool {
67 match self.scheme {
68 Scheme::Http => false,
69 Scheme::Https => true,
70 Scheme::WebSocket => false,
71 Scheme::SecureWebSocket => true,
72 }
73 }
74
75 pub fn scheme(&self) -> Scheme {
77 self.scheme
78 }
79
80 pub fn username(&self) -> Option<&str> {
82 Some(self.inner.username()).filter(|s| !s.is_empty())
83 }
84
85 pub fn password(&self) -> Option<&str> {
87 self.inner.password()
88 }
89
90 pub fn authority(&self) -> Option<String> {
93 self.username()
94 .map(|user| format!("{}:{}", user, self.password().unwrap_or_default()))
95 }
96
97 pub fn host(&self) -> &str {
99 self.inner.host_str().unwrap()
100 }
101
102 pub fn port(&self) -> u16 {
104 self.inner.port_or_known_default().unwrap()
105 }
106
107 pub fn path(&self) -> &str {
109 self.inner.path()
110 }
111}
112
113impl fmt::Display for Url {
114 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
115 write!(f, "{}", self.inner)
116 }
117}
118
119impl AsRef<url::Url> for Url {
120 fn as_ref(&self) -> &url::Url {
121 &self.inner
122 }
123}
124
125impl From<Url> for url::Url {
126 fn from(value: Url) -> Self {
127 value.inner
128 }
129}
130
131impl TryFrom<url::Url> for Url {
132 type Error = crate::Error;
133
134 fn try_from(url: url::Url) -> Result<Self, Self::Error> {
135 let scheme: Scheme = url.scheme().parse()?;
136
137 if url.host_str().is_none() {
138 return Err(Error::invalid_params(format!(
139 "URL is missing its host: {url}"
140 )));
141 }
142
143 if url.port_or_known_default().is_none() {
144 return Err(Error::invalid_params(format!(
145 "cannot determine appropriate port for URL: {url}"
146 )));
147 }
148
149 Ok(Self { inner: url, scheme })
150 }
151}
152
153impl Serialize for Url {
154 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
155 where
156 S: Serializer,
157 {
158 self.to_string().serialize(serializer)
159 }
160}
161
162impl<'de> Deserialize<'de> for Url {
163 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
164 where
165 D: Deserializer<'de>,
166 {
167 let s = String::deserialize(deserializer)?;
168 Url::from_str(&s).map_err(|e| D::Error::custom(e.to_string()))
169 }
170}
171
172#[cfg(test)]
173mod test {
174 use lazy_static::lazy_static;
175
176 use super::*;
177
178 struct ExpectedUrl {
179 scheme: Scheme,
180 host: String,
181 port: u16,
182 path: String,
183 username: Option<String>,
184 password: Option<String>,
185 }
186
187 lazy_static! {
188 static ref SUPPORTED_URLS: Vec<(String, ExpectedUrl)> = vec![
189 (
190 "tcp://127.0.0.1:26657".to_owned(),
191 ExpectedUrl {
192 scheme: Scheme::Http,
193 host: "127.0.0.1".to_string(),
194 port: 26657,
195 path: "".to_string(),
196 username: None,
197 password: None,
198 }
199 ),
200 (
201 "tcp://foo@127.0.0.1:26657".to_owned(),
202 ExpectedUrl {
203 scheme: Scheme::Http,
204 host: "127.0.0.1".to_string(),
205 port: 26657,
206 path: "".to_string(),
207 username: Some("foo".to_string()),
208 password: None,
209 }
210 ),
211 (
212 "tcp://foo:bar@127.0.0.1:26657".to_owned(),
213 ExpectedUrl {
214 scheme: Scheme::Http,
215 host: "127.0.0.1".to_string(),
216 port: 26657,
217 path: "".to_string(),
218 username: Some("foo".to_string()),
219 password: Some("bar".to_string()),
220 }
221 ),
222 (
223 "http://127.0.0.1:26657".to_owned(),
224 ExpectedUrl {
225 scheme: Scheme::Http,
226 host: "127.0.0.1".to_string(),
227 port: 26657,
228 path: "/".to_string(),
229 username: None,
230 password: None,
231 }
232 ),
233 (
234 "http://foo@127.0.0.1:26657".to_owned(),
235 ExpectedUrl {
236 scheme: Scheme::Http,
237 host: "127.0.0.1".to_string(),
238 port: 26657,
239 path: "/".to_string(),
240 username: Some("foo".to_string()),
241 password: None,
242 }
243 ),
244 (
245 "http://foo:bar@127.0.0.1:26657".to_owned(),
246 ExpectedUrl {
247 scheme: Scheme::Http,
248 host: "127.0.0.1".to_string(),
249 port: 26657,
250 path: "/".to_string(),
251 username: Some("foo".to_string()),
252 password: Some("bar".to_string()),
253 }
254 ),
255 (
256 "https://127.0.0.1:26657".to_owned(),
257 ExpectedUrl {
258 scheme: Scheme::Https,
259 host: "127.0.0.1".to_string(),
260 port: 26657,
261 path: "/".to_string(),
262 username: None,
263 password: None,
264 }
265 ),
266 (
267 "https://foo@127.0.0.1:26657".to_owned(),
268 ExpectedUrl {
269 scheme: Scheme::Https,
270 host: "127.0.0.1".to_string(),
271 port: 26657,
272 path: "/".to_string(),
273 username: Some("foo".to_string()),
274 password: None,
275 }
276 ),
277 (
278 "https://foo:bar@127.0.0.1:26657".to_owned(),
279 ExpectedUrl {
280 scheme: Scheme::Https,
281 host: "127.0.0.1".to_string(),
282 port: 26657,
283 path: "/".to_string(),
284 username: Some("foo".to_string()),
285 password: Some("bar".to_string()),
286 }
287 ),
288 (
289 "ws://127.0.0.1:26657/websocket".to_owned(),
290 ExpectedUrl {
291 scheme: Scheme::WebSocket,
292 host: "127.0.0.1".to_string(),
293 port: 26657,
294 path: "/websocket".to_string(),
295 username: None,
296 password: None,
297 }
298 ),
299 (
300 "ws://foo@127.0.0.1:26657/websocket".to_owned(),
301 ExpectedUrl {
302 scheme: Scheme::WebSocket,
303 host: "127.0.0.1".to_string(),
304 port: 26657,
305 path: "/websocket".to_string(),
306 username: Some("foo".to_string()),
307 password: None,
308 }
309 ),
310 (
311 "ws://foo:bar@127.0.0.1:26657/websocket".to_owned(),
312 ExpectedUrl {
313 scheme: Scheme::WebSocket,
314 host: "127.0.0.1".to_string(),
315 port: 26657,
316 path: "/websocket".to_string(),
317 username: Some("foo".to_string()),
318 password: Some("bar".to_string()),
319 }
320 ),
321 (
322 "wss://127.0.0.1:26657/websocket".to_owned(),
323 ExpectedUrl {
324 scheme: Scheme::SecureWebSocket,
325 host: "127.0.0.1".to_string(),
326 port: 26657,
327 path: "/websocket".to_string(),
328 username: None,
329 password: None,
330 }
331 ),
332 (
333 "wss://foo@127.0.0.1:26657/websocket".to_owned(),
334 ExpectedUrl {
335 scheme: Scheme::SecureWebSocket,
336 host: "127.0.0.1".to_string(),
337 port: 26657,
338 path: "/websocket".to_string(),
339 username: Some("foo".to_string()),
340 password: None,
341 }
342 ),
343 (
344 "wss://foo:bar@127.0.0.1:26657/websocket".to_owned(),
345 ExpectedUrl {
346 scheme: Scheme::SecureWebSocket,
347 host: "127.0.0.1".to_string(),
348 port: 26657,
349 path: "/websocket".to_string(),
350 username: Some("foo".to_string()),
351 password: Some("bar".to_string()),
352 }
353 )
354 ];
355 }
356
357 #[test]
358 fn parsing() {
359 for (url_str, expected) in SUPPORTED_URLS.iter() {
360 let u = Url::from_str(url_str).unwrap();
361 assert_eq!(expected.scheme, u.scheme(), "{url_str}");
362 assert_eq!(expected.host, u.host(), "{url_str}");
363 assert_eq!(expected.port, u.port(), "{url_str}");
364 assert_eq!(expected.path, u.path(), "{url_str}");
365 if let Some(n) = u.username() {
366 assert_eq!(expected.username.as_ref().unwrap(), n, "{url_str}");
367 } else {
368 assert!(expected.username.is_none(), "{}", url_str);
369 }
370 if let Some(pw) = u.password() {
371 assert_eq!(expected.password.as_ref().unwrap(), pw, "{url_str}");
372 } else {
373 assert!(expected.password.is_none(), "{}", url_str);
374 }
375 }
376 }
377}