better_url/
better_url.rs

1//! A wrapper around [`url::Url`] with extra metadata.
2
3use std::net::IpAddr;
4use std::str::FromStr;
5use std::ops::Deref;
6
7#[cfg(feature = "serde")]
8use serde::{Serialize, Deserialize, ser::Serializer, de::{Visitor, Deserializer, Error}};
9use url::{Url, PathSegmentsMut, ParseError};
10use thiserror::Error;
11
12pub mod host_details;
13pub use host_details::*;
14
15use crate::*;
16
17mod path_impl;
18pub use path_impl::*;
19mod domain_impl;
20pub use domain_impl::*;
21mod query_impl;
22pub use query_impl::*;
23
24/// A wrapper around a [`Url`] with extra metadata.
25///
26/// Currently the only included metadata is a [`HostDetails`], which currently only caches [PSL](https://publicsuffix.org/) information for more efficient reg domain, domain suffix, etc..
27#[derive(Clone)]
28pub struct BetterUrl {
29    /// The [`Url`].
30    url: Url,
31    /// The [`HostDetails`] of [`Self::url`].
32    host_details: Option<HostDetails>
33}
34
35/// The error [`BetterUrl::set_port`] returns when it fails.
36#[derive(Debug, Error)]
37#[error("Failed to set the port.")]
38pub struct SetPortError;
39
40/// The error [`BetterUrl::set_ip_host`] returns when it fails.
41#[derive(Debug, Error)]
42#[error("Failed to set the host to an IP.")]
43pub struct SetIpHostError;
44
45/// The error [`BetterUrl::set_password`] returns when it fails.
46#[derive(Debug, Error)]
47#[error("Failed to set the password.")]
48pub struct SetPasswordError;
49
50/// The error [`BetterUrl::set_username`] returns when it fails.
51#[derive(Debug, Error)]
52#[error("Failed to set the username.")]
53pub struct SetUsernameError;
54
55/// The error [`BetterUrl::set_scheme`] returns when it fails.
56#[derive(Debug, Error)]
57#[error("Failed to set the scheme.")]
58pub struct SetSchemeError;
59
60/// The error [`BetterUrl::set_host`] returns when it fails.
61#[derive(Debug, Error)]
62#[error(transparent)]
63pub struct SetHostError(#[from] pub ParseError);
64
65impl BetterUrl {
66    /// Parse a URL.
67    /// # Errors
68    /// If the call to [`Url::parse`] returns an error, that error is returned.
69    /// # Examples
70    /// ```
71    /// use better_url::*;
72    /// let url = BetterUrl::parse("https://example.com").unwrap();
73    /// ```
74    pub fn parse(value: &str) -> Result<Self, <Self as FromStr>::Err> {
75        Self::from_str(value)
76    }
77
78    /// Get the contained [`HostDetails`].
79    /// # Examples
80    /// ```
81    /// use better_url::*;
82    /// let url = BetterUrl::parse("https://example.com").unwrap();
83    ///
84    /// assert_eq!(url.host_details(), Some(&HostDetails::Domain(DomainDetails {middle_start: Some(0), suffix_start: Some(8), fqdn_period: None})));
85    ///
86    /// let url = BetterUrl::parse("https://127.0.0.1").unwrap();
87    ///
88    /// assert_eq!(url.host_details(), Some(&HostDetails::Ipv4(Ipv4Details {})));
89    ///
90    /// let url = BetterUrl::parse("https://[::1]").unwrap();
91    ///
92    /// assert_eq!(url.host_details(), Some(&HostDetails::Ipv6(Ipv6Details {})));
93    /// ```
94    pub fn host_details(&self) -> Option<&HostDetails> {
95        self.host_details.as_ref()
96    }
97
98    /// If [`Self::host_details`] returns [`HostDetails::Domain`], return it.
99    /// ```
100    /// use better_url::*;
101    /// let url = BetterUrl::parse("https://example.com").unwrap();
102    ///
103    /// assert_eq!(url.domain_details(), Some(&DomainDetails {middle_start: Some(0), suffix_start: Some(8), fqdn_period: None}));
104    /// assert_eq!(url.ipv4_details  (), None);
105    /// assert_eq!(url.ipv6_details  (), None);
106    /// ```
107    pub fn domain_details(&self) -> Option<&DomainDetails> {
108        self.host_details()?.domain_details()
109    }
110
111    /// If [`Self::host_details`] returns [`HostDetails::Ipv4`], return it.
112    /// ```
113    /// use better_url::*;
114    /// let url = BetterUrl::parse("https://127.0.0.1").unwrap();
115    ///
116    /// assert_eq!(url.domain_details(), None);
117    /// assert_eq!(url.ipv4_details  (), Some(&Ipv4Details {}));
118    /// assert_eq!(url.ipv6_details  (), None);
119    /// ```
120    pub fn ipv4_details(&self) -> Option<&Ipv4Details> {
121        self.host_details()?.ipv4_details()
122    }
123
124    /// If [`Self::host_details`] returns [`HostDetails::Ipv6`], return it.
125    /// ```
126    /// use better_url::*;
127    /// let url = BetterUrl::parse("https://[::1]").unwrap();
128    ///
129    /// assert_eq!(url.domain_details(), None);
130    /// assert_eq!(url.ipv4_details  (), None);
131    /// assert_eq!(url.ipv6_details  (), Some(&Ipv6Details {}));
132    /// ```
133    pub fn ipv6_details(&self) -> Option<&Ipv6Details> {
134        self.host_details()?.ipv6_details()
135    }
136
137    /// [`Url::host_str`] with any `www.` prefix and `.` suffix removed.
138    pub fn normalized_host(&self) -> Option<&str> {
139        let x = self.host_str()?;
140        let x = x.strip_prefix("www.").unwrap_or(x);
141        Some(x.strip_suffix(".").unwrap_or(x))
142    }
143
144    /// [`Url::set_scheme`].
145    /// # Errors
146    /// If the call to [`Url::set_scheme`] returns an error, returns the error [`SetSchemeError`].
147    pub fn set_scheme(&mut self, scheme: &str) -> Result<(), SetSchemeError> {
148        self.url.set_scheme(scheme).map_err(|()| SetSchemeError)
149    }
150
151    /// [`Url::set_username`].
152    /// # Errors
153    /// If the call to [`Url::set_username`] returns an error, returns the error [`SetUsernameError`].
154    pub fn set_username(&mut self, username: &str) -> Result<(), SetUsernameError> {
155        self.url.set_username(username).map_err(|()| SetUsernameError)
156    }
157
158    /// [`Url::set_password`].
159    /// # Errors
160    /// If the call to [`Url::set_password`] returns an error, returns the error [`SetPasswordError`].
161    pub fn set_password(&mut self, password: Option<&str>) -> Result<(), SetPasswordError> {
162        self.url.set_password(password).map_err(|()| SetPasswordError)
163    }
164
165    /// [`Self::set_host`] but use a precomputed [`HostDetails`].
166    fn set_host_with_known_details(&mut self, host: Option<&str>, host_details: Option<HostDetails>) -> Result<(), SetHostError> {
167        self.url.set_host(host)?;
168        self.host_details = host_details;
169        Ok(())
170    }
171
172    /// [`Url::set_host`].
173    /// # Errors
174    /// If the call to [`Url::set_host`] returns an error, the error is returned..
175    pub fn set_host(&mut self, host: Option<&str>) -> Result<(), SetHostError> {
176        self.url.set_host(host)?;
177        self.host_details = self.url.host().map(|host| HostDetails::from_host(&host));
178        Ok(())
179    }
180
181    /// [`Url::set_ip_host`].
182    /// # Errors
183    /// If the call to [`Url::set_ip_host`] returns an error, returns the error [`SetIpHostError`].
184    pub fn set_ip_host(&mut self, address: IpAddr) -> Result<(), SetIpHostError> {
185        self.url.set_ip_host(address).map_err(|()| SetIpHostError)?;
186        self.host_details = Some(HostDetails::from_ip_addr(address));
187        Ok(())
188    }
189
190    /// [`Url::set_port`].
191    /// # Errors
192    /// If the call to [`Url::set_port`] returns an error, returns the error [`SetPortError`].
193    pub fn set_port(&mut self, port: Option<u16>) -> Result<(), SetPortError> {
194        self.url.set_port(port).map_err(|()| SetPortError)
195    }
196
197    /// [`Url::set_fragment`].
198    pub fn set_fragment(&mut self, fragment: Option<&str>) {
199        self.url.set_fragment(fragment)
200    }
201}
202
203impl std::fmt::Debug for BetterUrl {
204    fn fmt(&self, f: &mut ::core::fmt::Formatter) -> ::core::fmt::Result {
205        write!(f, "{:?}", self.as_str())
206    }
207}
208
209impl std::fmt::Display for BetterUrl {
210    fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
211        self.url.fmt(formatter)
212    }
213}
214
215impl Deref for BetterUrl {
216    type Target = Url;
217
218    fn deref(&self) -> &Self::Target {
219        &self.url
220    }
221}
222
223
224impl PartialEq for BetterUrl {fn eq(&self, other: &Self) -> bool {self.url == other.url}}
225impl Eq for BetterUrl {}
226
227impl PartialEq<Url      > for BetterUrl {fn eq(&self, other: &Url      ) -> bool {&**self          ==    other}}
228impl PartialEq<String   > for BetterUrl {fn eq(&self, other: &String   ) -> bool {   self          == &**other}}
229impl PartialEq<str      > for BetterUrl {fn eq(&self, other: &str      ) -> bool {   self.as_str() ==    other}}
230impl PartialEq<&str     > for BetterUrl {fn eq(&self, other: &&str     ) -> bool {   self          ==   *other}}
231
232impl PartialEq<BetterUrl> for Url       {fn eq(&self, other: &BetterUrl) -> bool {other == self}}
233impl PartialEq<BetterUrl> for String    {fn eq(&self, other: &BetterUrl) -> bool {other == self}}
234impl PartialEq<BetterUrl> for str       {fn eq(&self, other: &BetterUrl) -> bool {other == self}}
235impl PartialEq<BetterUrl> for &str      {fn eq(&self, other: &BetterUrl) -> bool {other == self}}
236
237impl std::hash::Hash for BetterUrl {
238    /// Hashes the same as [`Url`].
239    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
240        std::hash::Hash::hash(&self.url, state)
241    }
242}
243
244impl PartialOrd for BetterUrl {
245    /// Ordered the same as [`Url`].
246    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
247        Some(self.cmp(other))
248    }
249}
250
251impl Ord for BetterUrl {
252    /// Ordered the same as [`Url`].
253    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
254        self.url.cmp(&other.url)
255    }
256}
257
258impl std::convert::AsRef<Url> for BetterUrl {
259    fn as_ref(&self) -> &Url {
260        &self.url
261    }
262}
263
264impl std::convert::AsRef<str> for BetterUrl {
265    fn as_ref(&self) -> &str {
266        self.url.as_ref()
267    }
268}
269
270impl FromStr for BetterUrl {
271    type Err = <Url as FromStr>::Err;
272
273    fn from_str(s: &str) -> Result<Self, Self::Err> {
274        Url::from_str(s).map(Into::into)
275    }
276}
277
278impl TryFrom<&str> for BetterUrl {
279    type Error = <Self as FromStr>::Err;
280
281    fn try_from(value: &str) -> Result<Self, Self::Error> {
282        Self::from_str(value)
283    }
284}
285
286impl From<Url> for BetterUrl {
287    fn from(value: Url) -> Self {
288        Self {
289            host_details: HostDetails::from_url(&value),
290            url: value
291        }
292    }
293}
294
295impl From<BetterUrl> for Url {
296    fn from(value: BetterUrl) -> Self {
297        value.url
298    }
299}
300
301impl From<BetterUrl> for String {
302    fn from(value: BetterUrl) -> Self {
303        value.url.into()
304    }
305}
306
307/// Serde helper for deserializing [`BetterUrl`].
308#[cfg(feature = "serde")]
309struct BetterUrlVisitor;
310
311#[cfg(feature = "serde")]
312impl<'de> Visitor<'de> for BetterUrlVisitor {
313    type Value = BetterUrl;
314
315    fn visit_str<E: Error>(self, s: &str) -> Result<Self::Value, E> {
316        s.try_into().map_err(E::custom)
317    }
318
319    fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
320        write!(formatter, "Expected a string")
321    }
322}
323
324#[cfg(feature = "serde")]
325impl<'de> Deserialize<'de> for BetterUrl {
326    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
327        deserializer.deserialize_any(BetterUrlVisitor)
328    }
329}
330
331#[cfg(feature = "serde")]
332impl Serialize for BetterUrl {
333    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
334        serializer.serialize_str(self.as_str())
335    }
336}