document_tree/
url.rs

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    // First character.
15    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/// The string representation of a URL, either absolute or relative, that has
27/// been verified as a valid URL on construction.
28#[derive(Clone, Debug, PartialEq, Serialize, JsonSchema)]
29#[serde(transparent)]
30pub struct Url(String);
31
32impl Url {
33    /// Parse an absolute URL.
34    ///
35    /// # Errors
36    /// Returns an error if the string is not a valid absolute URL.
37    pub fn parse_absolute(input: &str) -> Result<Self, ParseError> {
38        Ok(url::Url::parse(input)?.into())
39    }
40
41    /// Parse a relative path as URL.
42    ///
43    /// # Errors
44    /// Returns an error if the string is not a relative path or can’t be converted to an url.
45    #[allow(clippy::missing_panics_doc)]
46    pub fn parse_relative(input: &str) -> Result<Self, ParseError> {
47        // We're assuming that any scheme through which RsT documents are being
48        // accessed is a hierarchical scheme, and so we can parse relative to a
49        // random hierarchical URL.
50        if input.starts_with('/') || !starts_with_scheme(input) {
51            // Continue only if the parse succeeded, disregarding its result.
52            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            // If this is a URL at all, it's an absolute one.
59            // There's no appropriate variant of url::ParseError really.
60            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}