trillium_client/
into_url.rs

1use crate::{Error, Result};
2use std::{
3    net::{IpAddr, SocketAddr},
4    str::FromStr,
5};
6use trillium_server_common::url::{ParseError, Url};
7
8/// attempt to construct a url, with base if present
9pub trait IntoUrl {
10    /// attempt to construct a url, with base if present
11    fn into_url(self, base: Option<&Url>) -> Result<Url>;
12}
13
14impl IntoUrl for Url {
15    fn into_url(self, base: Option<&Url>) -> Result<Url> {
16        if self.cannot_be_a_base() {
17            return Err(Error::UnexpectedUriFormat);
18        }
19
20        if base.is_some_and(|base| !self.as_str().starts_with(base.as_str())) {
21            Err(Error::UnexpectedUriFormat)
22        } else {
23            Ok(self)
24        }
25    }
26}
27
28impl IntoUrl for &str {
29    fn into_url(self, base: Option<&Url>) -> Result<Url> {
30        match (Url::from_str(self), base) {
31            (Ok(url), base) => url.into_url(base),
32            (Err(ParseError::RelativeUrlWithoutBase), Some(base)) => base
33                .join(self.trim_start_matches('/'))
34                .map_err(|_| Error::UnexpectedUriFormat),
35            _ => Err(Error::UnexpectedUriFormat),
36        }
37    }
38}
39
40impl IntoUrl for String {
41    #[inline(always)]
42    fn into_url(self, base: Option<&Url>) -> Result<Url> {
43        self.as_str().into_url(base)
44    }
45}
46
47impl<S: AsRef<str>> IntoUrl for &[S] {
48    fn into_url(self, base: Option<&Url>) -> Result<Url> {
49        let Some(mut url) = base.cloned() else {
50            return Err(Error::UnexpectedUriFormat);
51        };
52        url.path_segments_mut()
53            .map_err(|_| Error::UnexpectedUriFormat)?
54            .pop_if_empty()
55            .extend(self);
56        Ok(url)
57    }
58}
59
60impl<S: AsRef<str>, const N: usize> IntoUrl for [S; N] {
61    fn into_url(self, base: Option<&Url>) -> Result<Url> {
62        self.as_slice().into_url(base)
63    }
64}
65
66impl<S: AsRef<str>> IntoUrl for Vec<S> {
67    fn into_url(self, base: Option<&Url>) -> Result<Url> {
68        self.as_slice().into_url(base)
69    }
70}
71
72impl IntoUrl for SocketAddr {
73    fn into_url(self, base: Option<&Url>) -> Result<Url> {
74        let scheme = if self.port() == 443 { "https" } else { "http" };
75        format!("{scheme}://{self}").into_url(base)
76    }
77}
78
79impl IntoUrl for IpAddr {
80    /// note that http is assumed regardless of port
81    fn into_url(self, base: Option<&Url>) -> Result<Url> {
82        match self {
83            IpAddr::V4(v4) => format!("http://{v4}"),
84            IpAddr::V6(v6) => format!("http://[{v6}]"),
85        }
86        .into_url(base)
87    }
88}