git_clone_canonical/
url.rs1use 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 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 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;