mit_commit/
trailers.rs

1use std::{convert::TryFrom, slice::Iter};
2
3use crate::{fragment::Fragment, trailer::Trailer};
4
5/// A Collection of `Trailer`
6#[derive(Debug, PartialEq, Eq, Clone, Default)]
7pub struct Trailers<'a> {
8    trailers: Vec<Trailer<'a>>,
9    iterator_index: usize,
10}
11
12impl Trailers<'_> {
13    /// Iterate over the [`Trailers`]
14    ///
15    /// # Examples
16    ///
17    /// ```
18    /// use mit_commit::{Trailer, Trailers};
19    /// let trailers = Trailers::from(vec![
20    ///     Trailer::new(
21    ///         "Co-authored-by".into(),
22    ///         "Billie Thompson <billie@example.com>".into(),
23    ///     ),
24    ///     Trailer::new(
25    ///         "Co-authored-by".into(),
26    ///         "Someone Else <someone@example.com>".into(),
27    ///     ),
28    ///     Trailer::new("Relates-to".into(), "#124".into()),
29    /// ]);
30    /// let mut iterator = trailers.iter();
31    ///
32    /// assert_eq!(
33    ///     iterator.next(),
34    ///     Some(&Trailer::new(
35    ///         "Co-authored-by".into(),
36    ///         "Billie Thompson <billie@example.com>".into()
37    ///     ))
38    /// );
39    /// assert_eq!(
40    ///     iterator.next(),
41    ///     Some(&Trailer::new(
42    ///         "Co-authored-by".into(),
43    ///         "Someone Else <someone@example.com>".into()
44    ///     ))
45    /// );
46    /// assert_eq!(
47    ///     iterator.next(),
48    ///     Some(&Trailer::new("Relates-to".into(), "#124".into()))
49    /// );
50    /// assert_eq!(iterator.next(), None);
51    /// ```
52    pub fn iter(&self) -> Iter<'_, Trailer<'_>> {
53        self.trailers.iter()
54    }
55
56    /// How many [`Trailers`] are there
57    ///
58    /// # Examples
59    ///
60    /// ```
61    /// use mit_commit::{Trailer, Trailers};
62    /// let trailers = Trailers::from(vec![
63    ///     Trailer::new(
64    ///         "Co-authored-by".into(),
65    ///         "Billie Thompson <billie@example.com>".into(),
66    ///     ),
67    ///     Trailer::new(
68    ///         "Co-authored-by".into(),
69    ///         "Someone Else <someone@example.com>".into(),
70    ///     ),
71    /// ]);
72    ///
73    /// assert_eq!(trailers.len(), 2)
74    /// ```
75    #[must_use]
76    pub const fn len(&self) -> usize {
77        self.trailers.len()
78    }
79
80    /// Are there no [`Trailers`]
81    ///
82    /// # Examples
83    ///
84    /// ```
85    /// use mit_commit::{Trailer, Trailers};
86    /// assert_eq!(
87    ///     Trailers::from(vec![
88    ///         Trailer::new(
89    ///             "Co-authored-by".into(),
90    ///             "Billie Thompson <billie@example.com>".into()
91    ///         ),
92    ///         Trailer::new(
93    ///             "Co-authored-by".into(),
94    ///             "Someone Else <someone@example.com>".into()
95    ///         ),
96    ///     ])
97    ///     .is_empty(),
98    ///     false
99    /// );
100    ///
101    /// let trailers: Vec<Trailer> = Vec::new();
102    /// assert_eq!(Trailers::from(trailers).is_empty(), true)
103    /// ```
104    #[must_use]
105    pub const fn is_empty(&self) -> bool {
106        self.trailers.is_empty()
107    }
108}
109
110impl<'a> IntoIterator for Trailers<'a> {
111    type Item = Trailer<'a>;
112    type IntoIter = std::vec::IntoIter<Trailer<'a>>;
113
114    /// Iterate over the [`Trailers`]
115    ///
116    /// # Examples
117    ///
118    /// ```
119    /// use mit_commit::{Trailer, Trailers};
120    /// let trailers = Trailers::from(vec![
121    ///     Trailer::new(
122    ///         "Co-authored-by".into(),
123    ///         "Billie Thompson <billie@example.com>".into(),
124    ///     ),
125    ///     Trailer::new(
126    ///         "Co-authored-by".into(),
127    ///         "Someone Else <someone@example.com>".into(),
128    ///     ),
129    ///     Trailer::new("Relates-to".into(), "#124".into()),
130    /// ]);
131    /// let mut iterator = trailers.into_iter();
132    ///
133    /// assert_eq!(
134    ///     iterator.next(),
135    ///     Some(Trailer::new(
136    ///         "Co-authored-by".into(),
137    ///         "Billie Thompson <billie@example.com>".into()
138    ///     ))
139    /// );
140    /// assert_eq!(
141    ///     iterator.next(),
142    ///     Some(Trailer::new(
143    ///         "Co-authored-by".into(),
144    ///         "Someone Else <someone@example.com>".into()
145    ///     ))
146    /// );
147    /// assert_eq!(
148    ///     iterator.next(),
149    ///     Some(Trailer::new("Relates-to".into(), "#124".into()))
150    /// );
151    /// assert_eq!(iterator.next(), None);
152    /// ```
153    fn into_iter(self) -> Self::IntoIter {
154        self.trailers.into_iter()
155    }
156}
157impl<'a> IntoIterator for &'a Trailers<'a> {
158    type IntoIter = Iter<'a, Trailer<'a>>;
159    type Item = &'a Trailer<'a>;
160
161    /// Iterate over the [`Trailers`]
162    ///
163    /// # Examples
164    ///
165    /// ```
166    /// use std::borrow::Borrow;
167    ///
168    /// use mit_commit::{Trailer, Trailers};
169    /// let trailers = Trailers::from(vec![
170    ///     Trailer::new(
171    ///         "Co-authored-by".into(),
172    ///         "Billie Thompson <billie@example.com>".into(),
173    ///     ),
174    ///     Trailer::new(
175    ///         "Co-authored-by".into(),
176    ///         "Someone Else <someone@example.com>".into(),
177    ///     ),
178    ///     Trailer::new("Relates-to".into(), "#124".into()),
179    /// ]);
180    /// let trailer_ref = trailers.borrow();
181    /// let mut iterator = trailer_ref.into_iter();
182    ///
183    /// assert_eq!(
184    ///     iterator.next(),
185    ///     Some(&Trailer::new(
186    ///         "Co-authored-by".into(),
187    ///         "Billie Thompson <billie@example.com>".into()
188    ///     ))
189    /// );
190    /// assert_eq!(
191    ///     iterator.next(),
192    ///     Some(&Trailer::new(
193    ///         "Co-authored-by".into(),
194    ///         "Someone Else <someone@example.com>".into()
195    ///     ))
196    /// );
197    /// assert_eq!(
198    ///     iterator.next(),
199    ///     Some(&Trailer::new("Relates-to".into(), "#124".into()))
200    /// );
201    /// assert_eq!(iterator.next(), None);
202    /// ```
203    fn into_iter(self) -> Self::IntoIter {
204        self.trailers.iter()
205    }
206}
207
208impl<'a> From<Vec<Trailer<'a>>> for Trailers<'a> {
209    fn from(trailers: Vec<Trailer<'a>>) -> Self {
210        Self {
211            trailers,
212            iterator_index: 0,
213        }
214    }
215}
216
217impl<'a> From<Trailers<'a>> for String {
218    fn from(trailers: Trailers<'a>) -> Self {
219        trailers
220            .trailers
221            .into_iter()
222            .map(Self::from)
223            .collect::<Vec<_>>()
224            .join("\n")
225    }
226}
227
228impl<'a> From<Vec<Fragment<'a>>> for Trailers<'a> {
229    fn from(ast: Vec<Fragment<'a>>) -> Self {
230        ast.into_iter()
231            .skip(1)
232            .filter_map(|values| {
233                if let Fragment::Body(body) = values {
234                    Some(body)
235                } else {
236                    None
237                }
238            })
239            .rev()
240            .filter_map(|body| {
241                if body.is_empty() {
242                    None
243                } else {
244                    Some(Trailer::try_from(body))
245                }
246            })
247            .take_while(Result::is_ok)
248            .flatten()
249            .collect::<Vec<Trailer<'_>>>()
250            .into_iter()
251            .rev()
252            .collect::<Vec<Trailer<'_>>>()
253            .into()
254    }
255}
256
257#[cfg(test)]
258mod tests {
259    use indoc::indoc;
260
261    use super::Trailers;
262    use crate::{Body, fragment::Fragment, trailer::Trailer};
263
264    #[test]
265    fn implements_iterator() {
266        let trailers = Trailers::from(vec![
267            Trailer::new(
268                "Co-authored-by".into(),
269                "Billie Thompson <billie@example.com>".into(),
270            ),
271            Trailer::new(
272                "Co-authored-by".into(),
273                "Someone Else <someone@example.com>".into(),
274            ),
275            Trailer::new("Relates-to".into(), "#124".into()),
276        ]);
277        let mut iterator = trailers.iter();
278
279        assert_eq!(
280            iterator.next(),
281            Some(&Trailer::new(
282                "Co-authored-by".into(),
283                "Billie Thompson <billie@example.com>".into(),
284            ))
285        );
286        assert_eq!(
287            iterator.next(),
288            Some(&Trailer::new(
289                "Co-authored-by".into(),
290                "Someone Else <someone@example.com>".into(),
291            ))
292        );
293        assert_eq!(
294            iterator.next(),
295            Some(&Trailer::new("Relates-to".into(), "#124".into()))
296        );
297        assert_eq!(iterator.next(), None);
298    }
299
300    #[test]
301    fn it_can_give_me_it_as_a_string() {
302        let trailers = Trailers::from(vec![Trailer::new(
303            "Co-authored-by".into(),
304            "Billie Thompson <billie@example.com>".into(),
305        )]);
306
307        assert_eq!(
308            String::from(trailers),
309            String::from("Co-authored-by: Billie Thompson <billie@example.com>")
310        );
311    }
312
313    #[test]
314    fn it_can_give_me_the_length() {
315        let trailers = Trailers::from(vec![
316            Trailer::new(
317                "Co-authored-by".into(),
318                "Billie Thompson <billie@example.com>".into(),
319            ),
320            Trailer::new(
321                "Co-authored-by".into(),
322                "Someone Else <someone@example.com>".into(),
323            ),
324        ]);
325
326        assert_eq!(trailers.len(), 2);
327    }
328
329    #[test]
330    fn it_can_tell_me_if_it_is_empty() {
331        assert!(
332            !Trailers::from(vec![
333                Trailer::new(
334                    "Co-authored-by".into(),
335                    "Billie Thompson <billie@example.com>".into()
336                ),
337                Trailer::new(
338                    "Co-authored-by".into(),
339                    "Someone Else <someone@example.com>".into()
340                ),
341            ])
342            .is_empty()
343        );
344
345        let trailers: Vec<Trailer<'_>> = Vec::new();
346        assert!(Trailers::from(trailers).is_empty());
347    }
348
349    #[test]
350    fn it_can_be_constructed_from_ast() {
351        let trailers = vec![
352            Fragment::Body(Body::from("Example Commit")),
353            Fragment::Body(Body::default()),
354            Fragment::Body(Body::from(indoc!(
355                "
356                    This is an example commit. This is to illustrate something for a test and would be
357                    pretty unusual to find in an actual git history.
358                    "
359            ))),
360            Fragment::Body(Body::default()),
361            Fragment::Body(Body::from(
362                "Co-authored-by: Billie Thompson <billie@example.com>",
363            )),
364            Fragment::Body(Body::from(
365                "Co-authored-by: Somebody Else <somebody@example.com>",
366            )),
367        ];
368
369        let expected: Trailers<'_> = vec![
370            Trailer::new(
371                "Co-authored-by".into(),
372                "Billie Thompson <billie@example.com>".into(),
373            ),
374            Trailer::new(
375                "Co-authored-by".into(),
376                "Somebody Else <somebody@example.com>".into(),
377            ),
378        ]
379        .into();
380
381        assert_eq!(Trailers::from(trailers), expected);
382    }
383
384    #[test]
385    fn it_can_be_constructed_from_ast_with_conventional_commits() {
386        let trailers = vec![
387            Fragment::Body(Body::from("feat: Example Commit")),
388            Fragment::Body(Body::default()),
389            Fragment::Body(Body::from(
390                "Co-authored-by: Billie Thompson <billie@example.com>",
391            )),
392            Fragment::Body(Body::from(
393                "Co-authored-by: Somebody Else <somebody@example.com>",
394            )),
395        ];
396
397        let expected: Trailers<'_> = vec![
398            Trailer::new(
399                "Co-authored-by".into(),
400                "Billie Thompson <billie@example.com>".into(),
401            ),
402            Trailer::new(
403                "Co-authored-by".into(),
404                "Somebody Else <somebody@example.com>".into(),
405            ),
406        ]
407        .into();
408
409        assert_eq!(Trailers::from(trailers), expected);
410    }
411}