1use std::{
2 collections::{btree_map::IntoIter, BTreeMap, HashSet},
3 convert::TryFrom,
4};
5
6use crate::mit::lib::{
7 author::Author,
8 errors::{DeserializeAuthorsError, SerializeAuthorsError},
9};
10
11#[derive(Debug, Eq, PartialEq, Clone, Default)]
13pub struct Authors<'a> {
14 pub authors: BTreeMap<String, Author<'a>>,
16}
17
18impl<'a> Authors<'a> {
19 #[must_use]
21 pub fn missing_initials(&'a self, authors_initials: Vec<&'a str>) -> Vec<&'a str> {
22 let configured: HashSet<_> = self.authors.keys().map(String::as_str).collect();
23 let from_cli: HashSet<_> = authors_initials.into_iter().collect();
24 from_cli.difference(&configured).copied().collect()
25 }
26
27 #[must_use]
29 pub const fn new(authors: BTreeMap<String, Author<'a>>) -> Self {
30 Self { authors }
31 }
32
33 #[must_use]
35 pub fn get(&self, author_initials: &'a [&'a str]) -> Vec<&'a Author<'_>> {
36 author_initials
37 .iter()
38 .filter_map(|initial| self.authors.get(*initial))
39 .collect()
40 }
41
42 #[must_use]
47 pub fn merge(&self, authors: &Self) -> Self {
48 let mut merged = self.authors.clone();
49 merged.extend(authors.authors.clone());
50 Self { authors: merged }
51 }
52
53 #[must_use]
57 pub fn example() -> Self {
58 let mut store = BTreeMap::new();
59 store.insert(
60 "ae".into(),
61 Author::new("Anyone Else".into(), "anyone@example.com".into(), None),
62 );
63 store.insert(
64 "se".into(),
65 Author::new("Someone Else".into(), "someone@example.com".into(), None),
66 );
67 store.insert(
68 "bt".into(),
69 Author::new(
70 "Billie Thompson".into(),
71 "billie@example.com".into(),
72 Some("0A46826A".into()),
73 ),
74 );
75 Self::new(store)
76 }
77}
78
79impl<'a> IntoIterator for Authors<'a> {
80 type Item = (String, Author<'a>);
81 type IntoIter = IntoIter<String, Author<'a>>;
82
83 fn into_iter(self) -> Self::IntoIter {
84 self.authors.into_iter()
85 }
86}
87
88impl<'a> TryFrom<&'a str> for Authors<'a> {
89 type Error = DeserializeAuthorsError;
90
91 fn try_from(input: &str) -> Result<Self, Self::Error> {
92 serde_yaml::from_str(input)
93 .or_else(|yaml_error| {
94 toml::from_str(input).map_err(|toml_error| {
95 DeserializeAuthorsError::new(input, &yaml_error, &toml_error)
96 })
97 })
98 .map(Self::new)
99 }
100}
101
102impl TryFrom<String> for Authors<'_> {
103 type Error = DeserializeAuthorsError;
104
105 fn try_from(input: String) -> Result<Self, Self::Error> {
106 serde_yaml::from_str(&input)
107 .or_else(|yaml_error| {
108 toml::from_str(&input).map_err(|toml_error| {
109 DeserializeAuthorsError::new(&input, &yaml_error, &toml_error)
110 })
111 })
112 .map(Authors::new)
113 }
114}
115
116impl<'a> TryFrom<Authors<'a>> for String {
117 type Error = SerializeAuthorsError;
118
119 fn try_from(value: Authors<'a>) -> Result<Self, Self::Error> {
120 toml::to_string(&value.authors).map_err(SerializeAuthorsError)
121 }
122}
123
124#[cfg(test)]
125mod tests {
126 #![allow(clippy::wildcard_imports)]
127
128 use std::{
129 collections::BTreeMap,
130 convert::{TryFrom, TryInto},
131 };
132
133 use indoc::indoc;
134
135 use crate::{
136 external::InMemory,
137 mit::{lib::author::Author, Authors},
138 };
139
140 #[test]
141 fn is_is_iterable() {
142 let mut store = BTreeMap::new();
143 store.insert(
144 "bt".into(),
145 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
146 );
147 let actual = Authors::new(store);
148
149 assert_eq!(
150 actual.into_iter().collect::<Vec<_>>(),
151 vec![(
152 "bt".to_string(),
153 Author::new("Billie Thompson".into(), "billie@example.com".into(), None)
154 )],
155 "Expected iterating to yield the single author with key 'bt'"
156 );
157 }
158
159 #[test]
160 fn it_can_get_an_author_in_it() {
161 let mut store = BTreeMap::new();
162 store.insert(
163 "bt".into(),
164 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
165 );
166 let actual = Authors::new(store);
167
168 assert_eq!(
169 actual.get(&["bt"]),
170 vec![&Author::new(
171 "Billie Thompson".into(),
172 "billie@example.com".into(),
173 None
174 )],
175 "Expected get by initials to return the matching author"
176 );
177 }
178
179 #[test]
180 fn i_can_get_multiple_authors_out_at_the_same_time() {
181 let mut store: BTreeMap<String, Author<'_>> = BTreeMap::new();
182 store.insert(
183 "bt".into(),
184 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
185 );
186 store.insert(
187 "se".into(),
188 Author::new("Somebody Else".into(), "somebody@example.com".into(), None),
189 );
190 let actual = Authors::new(store);
191
192 assert_eq!(
193 actual.get(&["bt"]),
194 vec![&Author::new(
195 "Billie Thompson".into(),
196 "billie@example.com".into(),
197 None
198 )],
199 "Expected get by 'bt' to return Billie Thompson"
200 );
201 assert_eq!(
202 actual.get(&["se"]),
203 vec![&Author::new(
204 "Somebody Else".into(),
205 "somebody@example.com".into(),
206 None
207 )],
208 "Expected get by 'se' to return Somebody Else"
209 );
210 }
211
212 #[test]
213 fn there_is_an_example_constructor() {
214 let mut store = BTreeMap::new();
215 store.insert(
216 "bt".into(),
217 Author::new(
218 "Billie Thompson".into(),
219 "billie@example.com".into(),
220 Some("0A46826A".into()),
221 ),
222 );
223 store.insert(
224 "se".into(),
225 Author::new("Someone Else".into(), "someone@example.com".into(), None),
226 );
227 store.insert(
228 "ae".into(),
229 Author::new("Anyone Else".into(), "anyone@example.com".into(), None),
230 );
231 let expected = Authors::new(store);
232
233 assert_eq!(
234 Authors::example(),
235 expected,
236 "Expected the example constructor to produce the predefined set of authors"
237 );
238 }
239
240 #[test]
241 fn merge_multiple_authors_together() {
242 let mut map1: BTreeMap<String, Author<'_>> = BTreeMap::new();
243 map1.insert(
244 "bt".into(),
245 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
246 );
247 map1.insert(
248 "se".into(),
249 Author::new("Someone Else".into(), "someone@example.com".into(), None),
250 );
251 let input1: Authors<'_> = Authors::new(map1);
252
253 let mut map2: BTreeMap<String, Author<'_>> = BTreeMap::new();
254 map2.insert(
255 "bt".into(),
256 Author::new("Billie Thompson".into(), "bt@example.com".into(), None),
257 );
258 map2.insert(
259 "ae".into(),
260 Author::new("Anyone Else".into(), "anyone@example.com".into(), None),
261 );
262 let input2: Authors<'_> = Authors::new(map2);
263
264 let mut expected_map: BTreeMap<String, Author<'_>> = BTreeMap::new();
265
266 expected_map.insert(
267 "bt".into(),
268 Author::new("Billie Thompson".into(), "bt@example.com".into(), None),
269 );
270 expected_map.insert(
271 "se".into(),
272 Author::new("Someone Else".into(), "someone@example.com".into(), None),
273 );
274 expected_map.insert(
275 "ae".into(),
276 Author::new("Anyone Else".into(), "anyone@example.com".into(), None),
277 );
278
279 let expected: Authors<'_> = Authors::new(expected_map);
280
281 assert_eq!(
282 expected,
283 input1.merge(&input2),
284 "Expected the merged authors to contain entries from both inputs, with input2 taking precedence"
285 );
286 }
287
288 #[test]
289 fn it_can_tell_me_if_initials_are_not_in() {
290 let mut store = BTreeMap::new();
291 store.insert(
292 "bt".into(),
293 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
294 );
295 let actual = Authors::new(store);
296
297 assert_eq!(
298 actual.missing_initials(vec!["bt", "an"]),
299 vec!["an"],
300 "Expected only 'an' to be missing since 'bt' is configured"
301 );
302 }
303
304 #[test]
305 fn must_be_valid_yaml() {
306 let actual: Result<_, _> = Authors::try_from("Hello I am invalid yaml : : :");
307 actual.unwrap_err();
308 }
309
310 #[test]
311 fn it_can_parse_a_standard_toml_file() {
312 let actual = Authors::try_from(indoc!(
313 "
314 [bt]
315 name = \"Billie Thompson\"
316 email = \"billie@example.com\"
317 "
318 ))
319 .expect("Failed to parse yaml");
320
321 let mut input: BTreeMap<String, Author<'_>> = BTreeMap::new();
322 input.insert(
323 "bt".into(),
324 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
325 );
326 let expected = Authors::new(input);
327
328 assert_eq!(
329 expected, actual,
330 "Expected the parsed TOML to match the author for 'bt'"
331 );
332 }
333
334 #[test]
335 fn an_empty_file_is_a_default_authors() {
336 let actual = Authors::try_from("").expect("Failed to parse yaml");
337
338 let expected = Authors::default();
339
340 assert_eq!(
341 expected, actual,
342 "Expected an empty file to parse as the default (empty) authors"
343 );
344 }
345
346 #[test]
347 fn it_can_parse_a_standard_yaml_file() {
348 let actual = Authors::try_from(indoc!(
349 "
350 ---
351 bt:
352 name: Billie Thompson
353 email: billie@example.com
354 "
355 ))
356 .expect("Failed to parse yaml");
357
358 let mut input: BTreeMap<String, Author<'_>> = BTreeMap::new();
359 input.insert(
360 "bt".into(),
361 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
362 );
363 let expected = Authors::new(input);
364
365 assert_eq!(
366 expected, actual,
367 "Expected the parsed YAML to match the author for 'bt'"
368 );
369 }
370
371 #[test]
372 fn yaml_files_can_contain_signing_keys() {
373 let actual = Authors::try_from(indoc!(
374 "
375 ---
376 bt:
377 name: Billie Thompson
378 email: billie@example.com
379 signingkey: 0A46826A
380 "
381 ))
382 .expect("Failed to parse yaml");
383
384 let mut expected_authors: BTreeMap<String, Author<'_>> = BTreeMap::new();
385 expected_authors.insert(
386 "bt".into(),
387 Author::new(
388 "Billie Thompson".into(),
389 "billie@example.com".into(),
390 Some("0A46826A".into()),
391 ),
392 );
393 let expected = Authors::new(expected_authors);
394
395 assert_eq!(
396 expected, actual,
397 "Expected the parsed YAML to include the signing key for 'bt'"
398 );
399 }
400
401 #[test]
402 fn it_converts_to_standard_toml() {
403 let mut map: BTreeMap<String, Author<'_>> = BTreeMap::new();
404 map.insert(
405 "bt".into(),
406 Author::new("Billie Thompson".into(), "billie@example.com".into(), None),
407 );
408 let actual: String = Authors::new(map).try_into().unwrap();
409 let expected = indoc!(
410 "
411 [bt]
412 name = \"Billie Thompson\"
413 email = \"billie@example.com\"
414 "
415 )
416 .to_string();
417
418 assert_eq!(
419 expected, actual,
420 "Expected the serialized TOML to match the standard format without signing key"
421 );
422 }
423
424 #[test]
425 fn it_includes_the_signing_key_if_set() {
426 let mut map: BTreeMap<String, Author<'_>> = BTreeMap::new();
427 map.insert(
428 "bt".into(),
429 Author::new(
430 "Billie Thompson".into(),
431 "billie@example.com".into(),
432 Some("0A46826A".into()),
433 ),
434 );
435 let actual: String = Authors::new(map).try_into().unwrap();
436 let expected = indoc!(
437 "
438 [bt]
439 name = \"Billie Thompson\"
440 email = \"billie@example.com\"
441 signingkey = \"0A46826A\"
442 "
443 )
444 .to_string();
445
446 assert_eq!(
447 expected, actual,
448 "Expected the serialized TOML to include the signing key when set"
449 );
450 }
451
452 #[test]
453 fn it_can_give_me_an_author() {
454 let mut strings: BTreeMap<String, String> = BTreeMap::new();
455 strings.insert("mit.author.config.zy.email".into(), "zy@example.com".into());
456 strings.insert("mit.author.config.zy.name".into(), "Z Y".into());
457 let vcs = InMemory::new(&mut strings);
458
459 let actual = Authors::try_from(&vcs).expect("Failed to read VCS config");
460 let expected_author = Author::new("Z Y".into(), "zy@example.com".into(), None);
461 let mut store = BTreeMap::new();
462 store.insert("zy".into(), expected_author);
463 let expected = Authors::new(store);
464 assert_eq!(
465 expected, actual,
466 "Expected the mit config to be {expected:?}, instead got {actual:?}"
467 );
468 }
469
470 #[test]
471 fn it_can_give_me_multiple_authors() {
472 let mut strings: BTreeMap<String, String> = BTreeMap::new();
473 strings.insert("mit.author.config.zy.email".into(), "zy@example.com".into());
474 strings.insert("mit.author.config.zy.name".into(), "Z Y".into());
475 strings.insert(
476 "mit.author.config.bt.email".into(),
477 "billie@example.com".into(),
478 );
479 strings.insert("mit.author.config.bt.name".into(), "Billie Thompson".into());
480 strings.insert("mit.author.config.bt.signingkey".into(), "ABC".into());
481 let vcs = InMemory::new(&mut strings);
482
483 let actual = Authors::try_from(&vcs).expect("Failed to read VCS config");
484 let mut store = BTreeMap::new();
485 store.insert(
486 "zy".into(),
487 Author::new("Z Y".into(), "zy@example.com".into(), None),
488 );
489 store.insert(
490 "bt".into(),
491 Author::new(
492 "Billie Thompson".into(),
493 "billie@example.com".into(),
494 Some("ABC".into()),
495 ),
496 );
497 let expected = Authors::new(store);
498 assert_eq!(
499 expected, actual,
500 "Expected the mit config to be {expected:?}, instead got {actual:?}"
501 );
502 }
503
504 #[test]
505 fn broken_authors_are_skipped() {
506 let mut strings: BTreeMap<String, String> = BTreeMap::new();
507 strings.insert("mit.author.config.zy.name".into(), "Z Y".into());
508 strings.insert(
509 "mit.author.config.bt.email".into(),
510 "billie@example.com".into(),
511 );
512 strings.insert("mit.author.config.bt.name".into(), "Billie Thompson".into());
513 strings.insert("mit.author.config.bt.signingkey".into(), "ABC".into());
514 let vcs = InMemory::new(&mut strings);
515
516 let actual = Authors::try_from(&vcs).expect("Failed to read VCS config");
517 let mut store = BTreeMap::new();
518 store.insert(
519 "bt".into(),
520 Author::new(
521 "Billie Thompson".into(),
522 "billie@example.com".into(),
523 Some("ABC".into()),
524 ),
525 );
526 let expected = Authors::new(store);
527 assert_eq!(
528 expected, actual,
529 "Expected the mit config to be {expected:?}, instead got {actual:?}"
530 );
531 }
532
533 #[test]
534 fn malformed_config_key_does_not_panic() {
535 let mut strings: BTreeMap<String, String> = BTreeMap::new();
536 strings.insert("mit.author.config.".into(), "value".into());
537 let vcs = InMemory::new(&mut strings);
538
539 let result = Authors::try_from(&vcs);
540 assert!(
541 result.is_err(),
542 "Expected an error for malformed config key"
543 );
544 }
545}