evobench_tools/serde_types/
git_url.rs

1use std::{fmt::Display, str::FromStr};
2
3use anyhow::{anyhow, bail};
4use serde::de::Visitor;
5
6use crate::utillib::path_resolve_home::path_resolve_home;
7
8#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, serde::Serialize)]
9pub struct GitUrl(String);
10
11impl GitUrl {
12    pub fn as_str(&self) -> &str {
13        &self.0
14    }
15}
16
17impl AsRef<str> for GitUrl {
18    fn as_ref(&self) -> &str {
19        self.as_str()
20    }
21}
22
23impl<'t> From<&'t GitUrl> for &'t str {
24    fn from(value: &'t GitUrl) -> Self {
25        value.as_str()
26    }
27}
28
29impl Display for GitUrl {
30    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
31        write!(f, "{}", self.0)
32    }
33}
34
35const ERR_MSG: &str = "a URL compatible with Git";
36
37impl FromStr for GitUrl {
38    type Err = anyhow::Error;
39
40    fn from_str(v: &str) -> Result<Self, Self::Err> {
41        let ok = Ok(GitUrl(v.to_owned()));
42
43        if let Some(rest) = v.strip_prefix("https://") {
44            if let Some((domain, other)) = rest.split_once('/') {
45                if domain.is_empty() {
46                    bail!("domain is empty")
47                }
48                if other.is_empty() {
49                    bail!("part after domain is empty")
50                }
51            } else {
52                bail!("expect a '/' between domain and location part")
53            }
54            return ok;
55        }
56
57        // XX is this correct, was it git:// ? All the rest the same or ?
58        if let Some(rest) = v.strip_prefix("git://") {
59            if let Some((domain, other)) = rest.split_once('/') {
60                if domain.is_empty() {
61                    bail!("domain is empty")
62                }
63                if other.is_empty() {
64                    bail!("part after domain is empty")
65                }
66            } else {
67                bail!("expect a '/' between domain and location part")
68            }
69            return ok;
70        }
71
72        if let Some(rest) = v.strip_prefix("file://") {
73            if rest.is_empty() {
74                bail!("empty file path given")
75            }
76            return ok;
77        }
78
79        if v.starts_with("/") || v.starts_with("../") {
80            // OK?
81            return ok;
82        }
83
84        if v.starts_with("~/") {
85            let path = path_resolve_home(v.as_ref())?;
86            let path_str = path
87                .to_str()
88                .ok_or_else(|| anyhow!("path {path:?} can't be represented as unicode string"))?;
89            return Ok(GitUrl(path_str.to_owned()));
90        }
91
92        if let Some((user, rest)) = v.split_once('@') {
93            if user.is_empty() {
94                bail!("user is empty")
95            }
96            if let Some((domain, _path)) = rest.split_once(':') {
97                if domain.is_empty() {
98                    bail!("domain is empty")
99                }
100                // I guess path *can* be empty, if the home dir is the repo.
101            } else {
102                bail!("missing ':' in ssh based Git URL")
103            }
104            return ok;
105        }
106
107        bail!("no match for any kind of Git url known to this code")
108    }
109}
110
111struct GitUrlVisitor;
112impl<'de> Visitor<'de> for GitUrlVisitor {
113    type Value = GitUrl;
114
115    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
116        formatter.write_str(ERR_MSG)
117    }
118
119    fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
120    where
121        E: serde::de::Error,
122    {
123        v.parse().map_err(E::custom)
124    }
125}
126
127impl<'de> serde::Deserialize<'de> for GitUrl {
128    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
129        deserializer.deserialize_str(GitUrlVisitor)
130    }
131}