1use std::{fmt::Display, str::FromStr};
2
3use chumsky::{prelude::*, Parser};
4use git_url_parse::{GitUrl, GitUrlParseError};
5use serde::{de, Deserialize, Deserializer};
6use thiserror::Error;
7
8const GITHUB: &str = "github";
9const GITLAB: &str = "gitlab";
10const SOURCEHUT: &str = "sourcehut";
11const CODEBERG: &str = "codeberg";
12
13#[derive(Debug, Error)]
14#[error("error parsing git source: {0:#?}")]
15pub struct ParseError(Vec<String>);
16
17#[derive(Debug, Clone)]
19pub struct GitUrlShorthand(GitUrl);
20
21impl GitUrlShorthand {
22 pub fn parse_with_prefix(s: &str) -> Result<Self, ParseError> {
23 prefix_parser()
24 .parse(s)
25 .into_result()
26 .map_err(|err| ParseError(err.into_iter().map(|e| e.to_string()).collect()))
27 }
28 pub fn repo_name() {}
29}
30
31impl FromStr for GitUrlShorthand {
32 type Err = ParseError;
33
34 fn from_str(s: &str) -> Result<Self, Self::Err> {
35 match parser()
36 .parse(s)
37 .into_result()
38 .map_err(|err| ParseError(err.into_iter().map(|e| e.to_string()).collect()))
39 {
40 Ok(url) => Ok(url),
41 Err(err) => match s.parse() {
42 Ok(url) => Ok(Self(url)),
44 Err(_) => Err(err),
45 },
46 }
47 }
48}
49
50impl<'de> Deserialize<'de> for GitUrlShorthand {
51 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
52 where
53 D: Deserializer<'de>,
54 {
55 String::deserialize(deserializer)?
56 .parse()
57 .map_err(de::Error::custom)
58 }
59}
60
61impl From<GitUrl> for GitUrlShorthand {
62 fn from(value: GitUrl) -> Self {
63 Self(value)
64 }
65}
66
67impl From<GitUrlShorthand> for GitUrl {
68 fn from(value: GitUrlShorthand) -> Self {
69 value.0
70 }
71}
72
73#[derive(Debug, Default)]
74enum GitHost {
75 #[default]
76 Github,
77 Gitlab,
78 Sourcehut,
79 Codeberg,
80}
81
82fn url_from_git_host(
83 host: GitHost,
84 owner: String,
85 repo: String,
86) -> Result<GitUrlShorthand, GitUrlParseError> {
87 let url_str = match host {
88 GitHost::Github => format!("https://github.com/{}/{}.git", owner, repo),
89 GitHost::Gitlab => format!("https://gitlab.com/{}/{}.git", owner, repo),
90 GitHost::Sourcehut => format!("https://git.sr.ht/~{}/{}", owner, repo),
91 GitHost::Codeberg => format!("https://codeberg.org/~{}/{}.git", owner, repo),
92 };
93 let url = url_str.parse()?;
94 Ok(GitUrlShorthand(url))
95}
96
97fn to_tuple<T>(v: Vec<T>) -> (T, T)
98where
99 T: Clone,
100{
101 (v[0].clone(), v[1].clone())
102}
103
104fn prefix_parser<'a>(
106) -> impl Parser<'a, &'a str, GitUrlShorthand, chumsky::extra::Err<Rich<'a, char>>> {
107 let git_host_prefix = just(GITHUB)
108 .or(just(GITLAB).or(just(SOURCEHUT).or(just(CODEBERG))))
109 .then_ignore(just(":"))
110 .map(|prefix| match prefix {
111 GITHUB => GitHost::Github,
112 GITLAB => GitHost::Gitlab,
113 SOURCEHUT => GitHost::Sourcehut,
114 CODEBERG => GitHost::Codeberg,
115 _ => unreachable!(),
116 })
117 .map_err(|err: Rich<'a, char>| {
118 let span = *err.span();
119 Rich::custom(span, "missing git host prefix. Expected 'github:', 'gitlab:', 'sourcehut:' or 'codeberg:'.")
120 });
121 let owner_repo = none_of('/')
122 .repeated()
123 .collect::<String>()
124 .separated_by(just('/'))
125 .exactly(2)
126 .collect::<Vec<String>>()
127 .map(to_tuple);
128 git_host_prefix
129 .then(owner_repo)
130 .try_map(|(host, (owner, repo)), span| {
131 let url = url_from_git_host(host, owner, repo).map_err(|err| {
132 Rich::custom(span, format!("error parsing git url shorthand: {}", err))
133 })?;
134 Ok(url)
135 })
136}
137
138fn parser<'a>() -> impl Parser<'a, &'a str, GitUrlShorthand, chumsky::extra::Err<Rich<'a, char>>> {
140 let git_host_prefix = just(GITHUB)
141 .or(just(GITLAB).or(just(SOURCEHUT).or(just(CODEBERG))))
142 .then_ignore(just(":"))
143 .or_not()
144 .map(|prefix| match prefix {
145 Some(GITHUB) => GitHost::Github,
146 Some(GITLAB) => GitHost::Gitlab,
147 Some(SOURCEHUT) => GitHost::Sourcehut,
148 Some(CODEBERG) => GitHost::Codeberg,
149 _ => GitHost::default(),
150 });
151 let owner_repo = none_of('/')
152 .repeated()
153 .collect::<String>()
154 .separated_by(just('/'))
155 .exactly(2)
156 .collect::<Vec<String>>()
157 .map(to_tuple);
158 git_host_prefix
159 .then(owner_repo)
160 .try_map(|(host, (owner, repo)), span| {
161 let url = url_from_git_host(host, owner, repo).map_err(|err| {
162 Rich::custom(span, format!("error parsing git url shorthand: {}", err))
163 })?;
164 Ok(url)
165 })
166}
167
168impl Display for GitUrlShorthand {
169 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
170 match (&self.0.host, &self.0.owner) {
171 (Some(host), Some(owner)) if host == "github.com" => {
172 format!("{}:{}/{}", GITHUB, owner, self.0.name)
173 }
174 (Some(host), Some(owner)) if host == "gitlab.com" => {
175 format!("{}:{}/{}", GITLAB, owner, self.0.name)
176 }
177 (Some(host), Some(owner)) if host == "git.sr.ht" => {
178 format!("{}:{}/{}", SOURCEHUT, owner.replace('~', ""), self.0.name)
179 }
180 (Some(host), Some(owner)) if host == "codeberg.org" => {
181 format!("{}:{}/{}", CODEBERG, owner.replace('~', ""), self.0.name)
182 }
183 _ => format!("{}", self.0),
184 }
185 .fmt(f)
186 }
187}
188
189#[cfg(test)]
190mod tests {
191 use super::*;
192
193 #[tokio::test]
194 async fn owner_repo_shorthand() {
195 let url_shorthand: GitUrlShorthand = "nvim-neorocks/lux".parse().unwrap();
196 assert_eq!(url_shorthand.0.owner, Some("nvim-neorocks".to_string()));
197 assert_eq!(url_shorthand.0.name, "lux".to_string());
198 }
199
200 #[tokio::test]
201 async fn github_shorthand() {
202 let url_shorthand_str = "github:nvim-neorocks/lux";
203 let url_shorthand: GitUrlShorthand = url_shorthand_str.parse().unwrap();
204 assert_eq!(url_shorthand.0.host, Some("github.com".to_string()));
205 assert_eq!(url_shorthand.0.owner, Some("nvim-neorocks".to_string()));
206 assert_eq!(url_shorthand.0.name, "lux".to_string());
207 assert_eq!(url_shorthand.to_string(), url_shorthand_str.to_string());
208 }
209
210 #[tokio::test]
211 async fn gitlab_shorthand() {
212 let url_shorthand_str = "gitlab:nvim-neorocks/lux";
213 let url_shorthand: GitUrlShorthand = url_shorthand_str.parse().unwrap();
214 assert_eq!(url_shorthand.0.host, Some("gitlab.com".to_string()));
215 assert_eq!(url_shorthand.0.owner, Some("nvim-neorocks".to_string()));
216 assert_eq!(url_shorthand.0.name, "lux".to_string());
217 assert_eq!(url_shorthand.to_string(), url_shorthand_str.to_string());
218 }
219
220 #[tokio::test]
221 async fn sourcehut_shorthand() {
222 let url_shorthand_str = "sourcehut:nvim-neorocks/lux";
223 let url_shorthand: GitUrlShorthand = url_shorthand_str.parse().unwrap();
224 assert_eq!(url_shorthand.0.host, Some("git.sr.ht".to_string()));
225 assert_eq!(url_shorthand.0.owner, Some("~nvim-neorocks".to_string()));
226 assert_eq!(url_shorthand.0.name, "lux".to_string());
227 assert_eq!(url_shorthand.to_string(), url_shorthand_str.to_string());
228 }
229
230 #[tokio::test]
231 async fn codeberg_shorthand() {
232 let url_shorthand_str = "codeberg:nvim-neorocks/lux";
233 let url_shorthand: GitUrlShorthand = url_shorthand_str.parse().unwrap();
234 assert_eq!(url_shorthand.0.host, Some("codeberg.org".to_string()));
235 assert_eq!(url_shorthand.0.owner, Some("~nvim-neorocks".to_string()));
236 assert_eq!(url_shorthand.0.name, "lux".to_string());
237 assert_eq!(url_shorthand.to_string(), url_shorthand_str.to_string());
238 }
239
240 #[tokio::test]
241 async fn regular_https_url() {
242 let url_shorthand: GitUrlShorthand =
243 "https://github.com/nvim-neorocks/lux.git".parse().unwrap();
244 assert_eq!(url_shorthand.0.host, Some("github.com".to_string()));
245 assert_eq!(url_shorthand.0.owner, Some("nvim-neorocks".to_string()));
246 assert_eq!(url_shorthand.0.name, "lux".to_string());
247 assert_eq!(
248 url_shorthand.to_string(),
249 "github:nvim-neorocks/lux".to_string()
250 );
251 }
252
253 #[tokio::test]
254 async fn regular_ssh_url() {
255 let url_str = "git@github.com:nvim-neorocks/lux.git";
256 let url_shorthand: GitUrlShorthand = url_str.parse().unwrap();
257 assert_eq!(url_shorthand.0.host, Some("github.com".to_string()));
258 assert_eq!(
259 url_shorthand.0.owner,
260 Some("git@github.com:nvim-neorocks".to_string())
261 );
262 assert_eq!(url_shorthand.0.name, "lux".to_string());
263 }
264
265 #[tokio::test]
266 async fn parse_with_prefix() {
267 GitUrlShorthand::parse_with_prefix("nvim-neorocks/lux").unwrap_err();
268 GitUrlShorthand::parse_with_prefix("github:nvim-neorocks/lux").unwrap();
269 GitUrlShorthand::parse_with_prefix("gitlab:nvim-neorocks/lux").unwrap();
270 GitUrlShorthand::parse_with_prefix("sourcehut:nvim-neorocks/lux").unwrap();
271 GitUrlShorthand::parse_with_prefix("codeberg:nvim-neorocks/lux").unwrap();
272 GitUrlShorthand::parse_with_prefix("bla:nvim-neorocks/lux").unwrap_err();
273 }
274}