Skip to main content

mit_commit_message_lints/mit/lib/
authors.rs

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/// Collection of authors
12#[derive(Debug, Eq, PartialEq, Clone, Default)]
13pub struct Authors<'a> {
14    /// A btree of the authors
15    pub authors: BTreeMap<String, Author<'a>>,
16}
17
18impl<'a> Authors<'a> {
19    /// From a list of initials get the ones that aren't in our config
20    #[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    /// Create a new author collection
28    #[must_use]
29    pub const fn new(authors: BTreeMap<String, Author<'a>>) -> Self {
30        Self { authors }
31    }
32
33    /// Get some authors by their initials
34    #[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    /// Merge two lists of authors
43    ///
44    /// This is used if the user has an author config file, and the authors are
45    /// also saved in the vcs config
46    #[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    /// Generate an example authors list
54    ///
55    /// Used to show the user what their config file might look like
56    #[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}