1use std::{borrow::Cow, str::FromStr};
2
3use crate::Error;
4
5#[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
28impl 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}