evobench_tools/serde_types/
git_url.rs1use 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 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 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 } 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}