ray/
url.rs

1use crate::RayError;
2use std::fmt;
3use url::Url;
4
5/// Parsed, validated URL for Ray link payloads.
6///
7/// URLs must be absolute (include a scheme, e.g. `https://example.com`).
8///
9/// ```no_run
10/// use ray::{RayError, RayUrl};
11///
12/// fn main() -> Result<(), RayError> {
13///     let url = RayUrl::parse("https://example.com")?;
14///     assert_eq!(url.as_str(), "https://example.com/");
15///     Ok(())
16/// }
17/// ```
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct RayUrl(Url);
20
21impl RayUrl {
22    /// Parse and validate a URL.
23    ///
24    /// If the scheme is missing, `https://` is assumed.
25    pub fn parse(input: impl AsRef<str>) -> Result<Self, RayError> {
26        Self::try_from(input.as_ref())
27    }
28
29    /// Return the URL as a string.
30    pub fn as_str(&self) -> &str {
31        self.0.as_str()
32    }
33}
34
35impl AsRef<str> for RayUrl {
36    fn as_ref(&self) -> &str {
37        self.0.as_str()
38    }
39}
40
41impl fmt::Display for RayUrl {
42    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
43        self.0.fmt(f)
44    }
45}
46
47impl TryFrom<&str> for RayUrl {
48    type Error = RayError;
49
50    fn try_from(input: &str) -> Result<Self, Self::Error> {
51        let trimmed = input.trim();
52        let url = match Url::parse(trimmed) {
53            Ok(url) => url,
54            Err(err) => {
55                if trimmed.contains("://") {
56                    return Err(RayError::invalid_input(
57                        "url",
58                        format!("invalid URL `{}`: {}", trimmed, err),
59                    ));
60                }
61
62                let candidate = format!("https://{}", trimmed);
63                Url::parse(&candidate).map_err(|_| {
64                    RayError::invalid_input("url", format!("invalid URL `{}`: {}", trimmed, err))
65                })?
66            }
67        };
68        Ok(Self(url))
69    }
70}
71
72impl TryFrom<String> for RayUrl {
73    type Error = RayError;
74
75    fn try_from(input: String) -> Result<Self, Self::Error> {
76        Self::try_from(input.as_str())
77    }
78}
79
80impl From<Url> for RayUrl {
81    fn from(url: Url) -> Self {
82        Self(url)
83    }
84}
85
86/// Accepted URL inputs for Ray link methods.
87///
88/// Supported inputs include `RayUrl`, `Url`, `&str`, and `String`.
89pub trait RayUrlInput {
90    /// Convert the input into a validated `RayUrl`.
91    fn try_into_ray_url(self) -> Result<RayUrl, RayError>;
92}
93
94impl RayUrlInput for RayUrl {
95    fn try_into_ray_url(self) -> Result<RayUrl, RayError> {
96        Ok(self)
97    }
98}
99
100impl RayUrlInput for Url {
101    fn try_into_ray_url(self) -> Result<RayUrl, RayError> {
102        Ok(RayUrl(self))
103    }
104}
105
106impl RayUrlInput for &RayUrl {
107    fn try_into_ray_url(self) -> Result<RayUrl, RayError> {
108        Ok(self.clone())
109    }
110}
111
112impl RayUrlInput for &Url {
113    fn try_into_ray_url(self) -> Result<RayUrl, RayError> {
114        Ok(RayUrl(self.clone()))
115    }
116}
117
118impl RayUrlInput for &str {
119    fn try_into_ray_url(self) -> Result<RayUrl, RayError> {
120        RayUrl::try_from(self)
121    }
122}
123
124impl RayUrlInput for String {
125    fn try_into_ray_url(self) -> Result<RayUrl, RayError> {
126        RayUrl::try_from(self)
127    }
128}
129
130impl RayUrlInput for &String {
131    fn try_into_ray_url(self) -> Result<RayUrl, RayError> {
132        RayUrl::try_from(self.as_str())
133    }
134}