git_clone_canonical/
url.rs

1use std::fmt;
2use std::str::FromStr;
3
4#[derive(Debug)]
5pub struct Url(url::Url);
6
7impl Url {
8    pub fn as_str(&self) -> &str {
9        self.0.as_str()
10    }
11
12    pub fn domain(&self) -> &str {
13        // Guaranteed to work if self was parsed with `FromStr`:
14        self.try_domain().unwrap()
15    }
16
17    pub fn path_segments(&self) -> impl Iterator<Item = &str> {
18        PathSegments(self.0.path_segments())
19    }
20
21    fn try_domain(&self) -> Result<&str, ParseError> {
22        self.0.domain().ok_or(ParseError::NoHost)
23    }
24}
25
26impl fmt::Display for Url {
27    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
28        self.0.fmt(f)
29    }
30}
31
32impl FromStr for Url {
33    type Err = ParseError;
34
35    fn from_str(s: &str) -> Result<Self, Self::Err> {
36        // Patch the input string from a "git-style" pseudo-url to a proper url.
37        //
38        // A git-style url is `user@host:path` which is patched to `ssh://user@host/path`
39        let patched = s
40            .split_once(':')
41            .map(|(prefix, suffix)| {
42                if suffix.starts_with("//") {
43                    s.to_string()
44                } else {
45                    format!("ssh://{prefix}/{suffix}")
46                }
47            })
48            .unwrap_or(s.to_string());
49
50        let u = Url(url::Url::from_str(&patched)?);
51        u.try_domain()?;
52        Ok(u)
53    }
54}
55
56#[derive(Debug, derive_more::From)]
57pub enum ParseError {
58    Url(url::ParseError),
59    NoHost,
60}
61
62impl std::error::Error for ParseError {}
63
64impl fmt::Display for ParseError {
65    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
66        use ParseError::*;
67
68        match self {
69            Url(e) => e.fmt(f),
70            NoHost => write!(f, "URL is missing required host domain"),
71        }
72    }
73}
74
75pub struct PathSegments<'a>(Option<std::str::Split<'a, char>>);
76
77impl<'a> Iterator for PathSegments<'a> {
78    type Item = &'a str;
79
80    fn next(&mut self) -> Option<Self::Item> {
81        self.0.as_mut().and_then(|it| it.next())
82    }
83}
84
85#[cfg(test)]
86mod tests;