junction_core/
url.rs

1use std::{borrow::Cow, str::FromStr};
2
3use crate::Error;
4
5/// An Uri with an `http` or `https` scheme and a non-empty `authority`.
6///
7/// The `authority` section of a `Url` must contains a hostname and may contain
8/// a port, but must not contain a username or password.
9///
10/// ```ascii
11/// https://example.com:123/path/data?key=value&key2=value2#fragid1
12/// ─┬───  ──────────┬──── ─────┬──── ───────┬─────────────────────
13///  │               │          │            │
14///  └─scheme        │     path─┘            │
15///                  │                       │
16///        authority─┘                 query─┘
17/// ```
18///
19/// There are no extra restrictions on the path or query components of a valid
20/// `Url`.
21#[derive(Clone, Debug, PartialEq, Eq)]
22pub struct Url {
23    scheme: http::uri::Scheme,
24    authority: http::uri::Authority,
25    path_and_query: http::uri::PathAndQuery,
26}
27
28// TODO: own error type here?
29
30impl std::fmt::Display for Url {
31    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
32        write!(
33            f,
34            "{scheme}://{authority}{path}",
35            scheme = self.scheme,
36            authority = self.authority,
37            path = self.path(),
38        )?;
39
40        if let Some(query) = self.query() {
41            write!(f, "?{query}")?;
42        }
43
44        Ok(())
45    }
46}
47
48impl Url {
49    pub fn new(uri: http::Uri) -> crate::Result<Self> {
50        let uri = uri.into_parts();
51
52        let Some(authority) = uri.authority else {
53            return Err(Error::invalid_url("missing hostname"));
54        };
55        if !authority.as_str().starts_with(authority.host()) {
56            return Err(Error::invalid_url(
57                "url must not contain a username or password",
58            ));
59        }
60
61        let scheme = match uri.scheme.as_ref().map(|s| s.as_str()) {
62            Some("http") | Some("https") => uri.scheme.unwrap(),
63            Some(_) => return Err(Error::invalid_url("unknown scheme")),
64            _ => return Err(Error::invalid_url("missing scheme")),
65        };
66        let path_and_query = uri
67            .path_and_query
68            .unwrap_or_else(|| http::uri::PathAndQuery::from_static("/"));
69
70        Ok(Self {
71            scheme,
72            authority,
73            path_and_query,
74        })
75    }
76}
77
78impl FromStr for Url {
79    type Err = Error;
80
81    fn from_str(s: &str) -> Result<Self, Self::Err> {
82        let uri = http::Uri::from_str(s).map_err(|e| Error::into_invalid_url(e.to_string()))?;
83
84        Self::new(uri)
85    }
86}
87
88impl Url {
89    pub fn scheme(&self) -> &str {
90        self.scheme.as_str()
91    }
92
93    pub fn hostname(&self) -> &str {
94        self.authority.host()
95    }
96
97    pub fn port(&self) -> Option<u16> {
98        self.authority.port_u16()
99    }
100
101    pub fn default_port(&self) -> u16 {
102        self.authority
103            .port_u16()
104            .unwrap_or_else(|| match self.scheme.as_ref() {
105                "https" => 443,
106                _ => 80,
107            })
108    }
109
110    pub fn path(&self) -> &str {
111        self.path_and_query.path()
112    }
113
114    pub fn query(&self) -> Option<&str> {
115        self.path_and_query.query()
116    }
117
118    pub fn request_uri(&self) -> &str {
119        self.path_and_query.as_str()
120    }
121
122    pub(crate) fn with_hostname(&self, hostname: &str) -> Result<Self, Error> {
123        let authority: Result<http::uri::Authority, http::uri::InvalidUri> =
124            match self.authority.port() {
125                Some(port) => format!("{hostname}:{port}").parse(),
126                None => hostname.parse(),
127            };
128
129        let authority = authority.map_err(|e| Error::into_invalid_url(e.to_string()))?;
130
131        Ok(Self {
132            authority,
133            scheme: self.scheme.clone(),
134            path_and_query: self.path_and_query.clone(),
135        })
136    }
137
138    pub(crate) fn authority(&self) -> Cow<'_, str> {
139        match self.authority.port() {
140            Some(_) => Cow::Borrowed(self.authority.as_str()),
141            None => Cow::Owned(format!(
142                "{host}:{port}",
143                host = self.authority.as_str(),
144                port = self.default_port()
145            )),
146        }
147    }
148}