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}