1use std::fmt;
2use std::str::FromStr;
3
4use schemars::JsonSchema;
5use serde_derive::Serialize;
6use url::{self, ParseError};
7
8fn starts_with_scheme(input: &str) -> bool {
9 let scheme = input.split(':').next().unwrap();
10 if scheme == input || scheme.is_empty() {
11 return false;
12 }
13 let mut chars = input.chars();
14 if !chars.next().unwrap().is_ascii_alphabetic() {
16 return false;
17 }
18 for ch in chars {
19 if !ch.is_ascii_alphanumeric() && ch != '+' && ch != '-' && ch != '.' {
20 return false;
21 }
22 }
23 true
24}
25
26#[derive(Clone, Debug, PartialEq, Serialize, JsonSchema)]
29#[serde(transparent)]
30pub struct Url(String);
31
32impl Url {
33 pub fn parse_absolute(input: &str) -> Result<Self, ParseError> {
38 Ok(url::Url::parse(input)?.into())
39 }
40
41 #[allow(clippy::missing_panics_doc)]
46 pub fn parse_relative(input: &str) -> Result<Self, ParseError> {
47 if input.starts_with('/') || !starts_with_scheme(input) {
51 let random_base_url = url::Url::parse("https://a/b").unwrap();
53 url::Url::options()
54 .base_url(Some(&random_base_url))
55 .parse(input)?;
56 Ok(Url(input.into()))
57 } else {
58 Err(ParseError::SetHostOnCannotBeABaseUrl)
61 }
62 }
63 #[must_use]
64 pub fn as_str(&self) -> &str {
65 self.0.as_str()
66 }
67}
68
69impl From<url::Url> for Url {
70 fn from(url: url::Url) -> Self {
71 Url(url.into())
72 }
73}
74
75impl fmt::Display for Url {
76 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
77 write!(f, "{}", self.as_str())
78 }
79}
80
81impl FromStr for Url {
82 type Err = ParseError;
83 fn from_str(input: &str) -> Result<Self, Self::Err> {
84 Url::parse_absolute(input).or_else(|_| Url::parse_relative(input))
85 }
86}