1use crate::Mode;
3use serde::{Deserialize, Serialize};
4
5#[derive(Debug, Clone, PartialEq, Eq)]
6pub enum ShortnameError {
8 NoPathSegments,
10 NoValidSegments,
12}
13
14impl std::fmt::Display for ShortnameError {
15 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
16 match self {
17 ShortnameError::NoPathSegments => write!(f, "URL has no path segments"),
18 ShortnameError::NoValidSegments => write!(f, "No non-empty path segments found"),
19 }
20 }
21}
22
23impl std::error::Error for ShortnameError {}
24
25#[derive(Debug, Clone, Deserialize, Serialize)]
26pub struct Candidate {
28 pub url: url::Url,
30
31 pub name: Option<String>,
33
34 pub branch: Option<String>,
36
37 pub subpath: Option<std::path::PathBuf>,
39
40 pub paths: Option<Vec<String>>,
42
43 #[serde(rename = "default-mode")]
44 pub default_mode: Option<Mode>,
46}
47
48impl Candidate {
49 pub fn shortname(&self) -> Result<std::borrow::Cow<'_, str>, ShortnameError> {
51 match &self.name {
52 Some(name) => Ok(std::borrow::Cow::Borrowed(name)),
53 None => {
54 if let Some(segments) = self.url.path_segments() {
55 let segments: Vec<_> = segments.collect();
56
57 let last_non_empty = segments.iter().rev().find(|s| !s.is_empty());
59
60 match last_non_empty {
61 Some(segment) => Ok(std::borrow::Cow::Owned(segment.to_string())),
62 None => Err(ShortnameError::NoValidSegments),
63 }
64 } else {
65 Err(ShortnameError::NoPathSegments)
66 }
67 }
68 }
69 }
70}
71
72#[derive(Debug, Clone, Default)]
73pub struct Candidates(Vec<Candidate>);
75
76impl Candidates {
77 pub fn from_path(path: &std::path::Path) -> std::io::Result<Self> {
79 let f = std::fs::File::open(path)?;
80 let candidates: Vec<Candidate> = serde_yaml::from_reader(f).unwrap();
81 Ok(Self(candidates))
82 }
83
84 pub fn candidates(&self) -> &[Candidate] {
86 self.0.as_slice()
87 }
88
89 pub fn iter(&self) -> impl Iterator<Item = &Candidate> {
91 self.0.iter()
92 }
93
94 pub fn new() -> Self {
96 Self(Vec::new())
97 }
98}
99
100impl TryFrom<serde_yaml::Value> for Candidates {
101 type Error = serde_yaml::Error;
102
103 fn try_from(yaml: serde_yaml::Value) -> Result<Self, Self::Error> {
104 Ok(Self(serde_yaml::from_value(yaml)?))
105 }
106}
107
108impl From<Vec<Candidate>> for Candidates {
109 fn from(candidates: Vec<Candidate>) -> Self {
110 Self(candidates)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 #[test]
118 fn test_read() {
119 let td = tempfile::tempdir().unwrap();
120 let path = td.path().join("candidates.yaml");
121 std::fs::write(
122 &path,
123 r#"---
124 - url: https://github.com/jelmer/dulwich
125 - name: samba
126 url: https://git.samba.org/samba.git
127 "#,
128 )
129 .unwrap();
130 let candidates = Candidates::from_path(&path).unwrap();
131 assert_eq!(candidates.candidates().len(), 2);
132 assert_eq!(
133 candidates.candidates()[0].url,
134 url::Url::parse("https://github.com/jelmer/dulwich").unwrap()
135 );
136 assert_eq!(
137 candidates.candidates()[1].url,
138 url::Url::parse("https://git.samba.org/samba.git").unwrap()
139 );
140 assert_eq!(candidates.candidates()[1].name, Some("samba".to_string()));
141 }
142
143 #[test]
144 fn test_shortname() {
145 let candidate = Candidate {
146 url: url::Url::parse("https://github.com/jelmer/dulwich").unwrap(),
147 name: None,
148 branch: None,
149 subpath: None,
150 paths: None,
151 default_mode: None,
152 };
153
154 assert_eq!(candidate.shortname().unwrap(), "dulwich");
155 }
156
157 #[test]
158 fn test_shortname_stored() {
159 let candidate = Candidate {
160 url: url::Url::parse("https://github.com/jelmer/dulwich").unwrap(),
161 name: Some("foo".to_string()),
162 branch: None,
163 subpath: None,
164 paths: None,
165 default_mode: None,
166 };
167
168 assert_eq!(candidate.shortname().unwrap(), "foo");
169 }
170
171 #[test]
172 fn test_shortname_cow_behavior() {
173 use std::borrow::Cow;
174
175 let candidate_with_name = Candidate {
177 url: url::Url::parse("https://github.com/jelmer/dulwich").unwrap(),
178 name: Some("myproject".to_string()),
179 branch: None,
180 subpath: None,
181 paths: None,
182 default_mode: None,
183 };
184
185 let shortname = candidate_with_name.shortname().unwrap();
186 assert!(matches!(shortname, Cow::Borrowed(_)));
187 assert_eq!(shortname, "myproject");
188
189 let candidate_without_name = Candidate {
191 url: url::Url::parse("https://github.com/jelmer/dulwich").unwrap(),
192 name: None,
193 branch: None,
194 subpath: None,
195 paths: None,
196 default_mode: None,
197 };
198
199 let shortname = candidate_without_name.shortname().unwrap();
200 assert!(matches!(shortname, Cow::Owned(_)));
201 assert_eq!(shortname, "dulwich");
202 }
203
204 #[test]
205 fn test_shortname_edge_cases() {
206 let candidate_no_path = Candidate {
208 url: url::Url::parse("https://github.com/").unwrap(),
209 name: None,
210 branch: None,
211 subpath: None,
212 paths: None,
213 default_mode: None,
214 };
215 assert!(candidate_no_path.shortname().is_err());
216 assert_eq!(
217 candidate_no_path.shortname().unwrap_err(),
218 ShortnameError::NoValidSegments
219 );
220
221 let candidate_trailing_slash = Candidate {
223 url: url::Url::parse("https://github.com/jelmer/project/").unwrap(),
224 name: None,
225 branch: None,
226 subpath: None,
227 paths: None,
228 default_mode: None,
229 };
230 assert_eq!(candidate_trailing_slash.shortname().unwrap(), "project");
231 }
232
233 #[test]
234 fn test_candidates_new() {
235 let candidates = Candidates::new();
236 assert_eq!(candidates.candidates().len(), 0);
237 }
238
239 #[test]
240 fn test_candidates_from_vec() {
241 let candidate1 = Candidate {
242 url: url::Url::parse("https://github.com/jelmer/dulwich").unwrap(),
243 name: Some("dulwich".to_string()),
244 branch: None,
245 subpath: None,
246 paths: None,
247 default_mode: None,
248 };
249
250 let candidate2 = Candidate {
251 url: url::Url::parse("https://github.com/jelmer/silver-platter").unwrap(),
252 name: Some("silver-platter".to_string()),
253 branch: None,
254 subpath: None,
255 paths: None,
256 default_mode: None,
257 };
258
259 let candidates_vec = vec![candidate1, candidate2];
260 let candidates = Candidates::from(candidates_vec);
261
262 assert_eq!(candidates.candidates().len(), 2);
263 assert_eq!(candidates.candidates()[0].name, Some("dulwich".to_string()));
264 assert_eq!(
265 candidates.candidates()[1].name,
266 Some("silver-platter".to_string())
267 );
268 }
269
270 #[test]
271 fn test_candidates_iter() {
272 let candidate1 = Candidate {
273 url: url::Url::parse("https://github.com/jelmer/dulwich").unwrap(),
274 name: Some("dulwich".to_string()),
275 branch: None,
276 subpath: None,
277 paths: None,
278 default_mode: None,
279 };
280
281 let candidate2 = Candidate {
282 url: url::Url::parse("https://github.com/jelmer/silver-platter").unwrap(),
283 name: Some("silver-platter".to_string()),
284 branch: None,
285 subpath: None,
286 paths: None,
287 default_mode: None,
288 };
289
290 let candidates = Candidates::from(vec![candidate1, candidate2]);
291
292 let names: Vec<String> = candidates.iter().map(|c| c.name.clone().unwrap()).collect();
293
294 assert_eq!(
295 names,
296 vec!["dulwich".to_string(), "silver-platter".to_string()]
297 );
298 }
299
300 #[test]
301 fn test_try_from_yaml() {
302 let yaml = serde_yaml::from_str::<serde_yaml::Value>(
303 r#"
304 - url: https://github.com/jelmer/dulwich
305 name: dulwich
306 - url: https://github.com/jelmer/silver-platter
307 name: silver-platter
308 branch: main
309 "#,
310 )
311 .unwrap();
312
313 let candidates = Candidates::try_from(yaml).unwrap();
314
315 assert_eq!(candidates.candidates().len(), 2);
316 assert_eq!(candidates.candidates()[0].name, Some("dulwich".to_string()));
317 assert_eq!(candidates.candidates()[1].branch, Some("main".to_string()));
318 }
319
320 #[test]
321 fn test_candidate_with_paths() {
322 let td = tempfile::tempdir().unwrap();
323 let path = td.path().join("candidates.yaml");
324 std::fs::write(
325 &path,
326 r#"---
327 - url: https://github.com/org/monorepo
328 name: monorepo
329 paths:
330 - frontend
331 - backend
332 - docs
333 "#,
334 )
335 .unwrap();
336 let candidates = Candidates::from_path(&path).unwrap();
337 assert_eq!(candidates.candidates().len(), 1);
338 let candidate = &candidates.candidates()[0];
339 assert_eq!(candidate.name, Some("monorepo".to_string()));
340 assert_eq!(
341 candidate.paths,
342 Some(vec![
343 "frontend".to_string(),
344 "backend".to_string(),
345 "docs".to_string()
346 ])
347 );
348 }
349}