mit_commit/
bodies.rs

1use std::{
2    convert::TryFrom,
3    fmt,
4    fmt::{Display, Formatter},
5    slice::Iter,
6    vec::IntoIter,
7};
8
9use crate::{body::Body, fragment::Fragment, trailer::Trailer};
10
11/// A collection of body paragraphs from a commit message.
12///
13/// This struct represents multiple body paragraphs that make up the content of a commit message.
14///
15/// # Examples
16///
17/// ```
18/// use mit_commit::{Bodies, Body, Subject};
19///
20/// let bodies: Vec<Body> = Vec::default();
21/// assert_eq!(None, Bodies::from(bodies).first());
22///
23/// let bodies: Vec<Body> = vec![
24///     Body::from("First"),
25///     Body::from("Second"),
26///     Body::from("Third"),
27/// ];
28/// assert_eq!(Some(Body::from("First")), Bodies::from(bodies).first());
29/// ```
30#[derive(Debug, PartialEq, Eq, Clone, Default)]
31pub struct Bodies<'a> {
32    bodies: Vec<Body<'a>>,
33}
34
35impl Bodies<'_> {
36    /// Get the first [`Body`] in this list of [`Bodies`]
37    ///
38    /// # Arguments
39    ///
40    /// * `self` - The Bodies collection to get the first element from
41    ///
42    /// # Returns
43    ///
44    /// The first Body in the list, or None if the list is empty
45    ///
46    /// # Examples
47    ///
48    /// ```
49    /// use mit_commit::{Bodies, Body, Subject};
50    ///
51    /// let bodies: Vec<Body> = Vec::default();
52    /// assert_eq!(None, Bodies::from(bodies).first());
53    ///
54    /// let bodies: Vec<Body> = vec![
55    ///     Body::from("First"),
56    ///     Body::from("Second"),
57    ///     Body::from("Third"),
58    /// ];
59    /// assert_eq!(Some(Body::from("First")), Bodies::from(bodies).first());
60    /// ```
61    #[must_use]
62    pub fn first(&self) -> Option<Body<'_>> {
63        self.bodies.first().cloned()
64    }
65
66    /// Iterate over the [`Body`] in the [`Bodies`]
67    ///
68    /// # Arguments
69    ///
70    /// * `self` - The Bodies collection to iterate over
71    ///
72    /// # Returns
73    ///
74    /// An iterator over the Body elements in the Bodies collection
75    ///
76    /// # Examples
77    ///
78    /// ```
79    /// use mit_commit::{Bodies, Body};
80    /// let bodies = Bodies::from(vec![
81    ///     Body::from("Body 1"),
82    ///     Body::from("Body 2"),
83    ///     Body::from("Body 3"),
84    /// ]);
85    /// let mut iterator = bodies.iter();
86    ///
87    /// assert_eq!(iterator.next(), Some(&Body::from("Body 1")));
88    /// assert_eq!(iterator.next(), Some(&Body::from("Body 2")));
89    /// assert_eq!(iterator.next(), Some(&Body::from("Body 3")));
90    /// assert_eq!(iterator.next(), None);
91    /// ```
92    pub fn iter(&self) -> Iter<'_, Body<'_>> {
93        self.bodies.iter()
94    }
95}
96
97impl<'a> IntoIterator for Bodies<'a> {
98    type Item = Body<'a>;
99    type IntoIter = IntoIter<Body<'a>>;
100
101    /// Iterate over the [`Body`] in the [`Bodies`]
102    ///
103    /// # Arguments
104    ///
105    /// * `self` - The Bodies collection to consume and iterate over
106    ///
107    /// # Returns
108    ///
109    /// An iterator that takes ownership of the Bodies collection
110    ///
111    /// # Examples
112    ///
113    /// ```
114    /// use mit_commit::{Bodies, Body};
115    /// let bodies = Bodies::from(vec![
116    ///     Body::from("Body 1"),
117    ///     Body::from("Body 2"),
118    ///     Body::from("Body 3"),
119    /// ]);
120    /// let mut iterator = bodies.into_iter();
121    ///
122    /// assert_eq!(iterator.next(), Some(Body::from("Body 1")));
123    /// assert_eq!(iterator.next(), Some(Body::from("Body 2")));
124    /// assert_eq!(iterator.next(), Some(Body::from("Body 3")));
125    /// assert_eq!(iterator.next(), None);
126    /// ```
127    fn into_iter(self) -> Self::IntoIter {
128        self.bodies.into_iter()
129    }
130}
131
132impl<'a> IntoIterator for &'a Bodies<'a> {
133    type IntoIter = Iter<'a, Body<'a>>;
134    type Item = &'a Body<'a>;
135
136    /// Iterate over the [`Body`] in the [`Bodies`]
137    ///
138    /// # Arguments
139    ///
140    /// * `self` - A reference to the Bodies collection to iterate over
141    ///
142    /// # Returns
143    ///
144    /// An iterator over references to the Body elements in the Bodies collection
145    ///
146    /// # Examples
147    ///
148    /// ```
149    /// use std::borrow::Borrow;
150    ///
151    /// use mit_commit::{Bodies, Body};
152    /// let bodies = Bodies::from(vec![
153    ///     Body::from("Body 1"),
154    ///     Body::from("Body 2"),
155    ///     Body::from("Body 3"),
156    /// ]);
157    /// let bodies_ref = bodies.borrow();
158    /// let mut iterator = bodies_ref.into_iter();
159    ///
160    /// assert_eq!(iterator.next(), Some(&Body::from("Body 1")));
161    /// assert_eq!(iterator.next(), Some(&Body::from("Body 2")));
162    /// assert_eq!(iterator.next(), Some(&Body::from("Body 3")));
163    /// assert_eq!(iterator.next(), None);
164    /// ```
165    fn into_iter(self) -> Self::IntoIter {
166        self.bodies.iter()
167    }
168}
169
170impl Display for Bodies<'_> {
171    /// Render the [`Bodies`] as text
172    ///
173    /// # Arguments
174    ///
175    /// * `self` - The Bodies collection to format
176    /// * `f` - The formatter to write the formatted string to
177    ///
178    /// # Returns
179    ///
180    /// A string representation of the Bodies with each Body separated by double newlines
181    ///
182    /// # Examples
183    ///
184    /// ```
185    /// use mit_commit::{Bodies, Body};
186    /// let bodies = Bodies::from(vec![
187    ///     Body::from("Body 1"),
188    ///     Body::from("Body 2"),
189    ///     Body::from("Body 3"),
190    /// ]);
191    ///
192    /// assert_eq!(format!("{}", bodies), "Body 1\n\nBody 2\n\nBody 3");
193    /// ```
194    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
195        if self.bodies.is_empty() {
196            return Ok(());
197        }
198
199        let mut iter = self.bodies.iter();
200        if let Some(first) = iter.next() {
201            write!(f, "{first}")?;
202            for body in iter {
203                write!(f, "\n\n{body}")?;
204            }
205        }
206
207        Ok(())
208    }
209}
210
211impl<'a> From<Vec<Body<'a>>> for Bodies<'a> {
212    /// Combine a [`Vec`] of [`Body`] into [`Bodies`]
213    ///
214    /// # Arguments
215    ///
216    /// * `bodies` - A vector of Body objects to be combined into a Bodies collection
217    ///
218    /// # Returns
219    ///
220    /// A new Bodies instance containing all the provided Body objects
221    ///
222    /// # Examples
223    ///
224    /// ```
225    /// use mit_commit::{Bodies, Body};
226    /// let bodies = Bodies::from(vec![
227    ///     Body::from("Body 1"),
228    ///     Body::from("Body 2"),
229    ///     Body::from("Body 3"),
230    /// ]);
231    /// let mut iterator = bodies.into_iter();
232    ///
233    /// assert_eq!(iterator.next(), Some(Body::from("Body 1")));
234    /// assert_eq!(iterator.next(), Some(Body::from("Body 2")));
235    /// assert_eq!(iterator.next(), Some(Body::from("Body 3")));
236    /// assert_eq!(iterator.next(), None);
237    /// ```
238    fn from(bodies: Vec<Body<'a>>) -> Self {
239        Self { bodies }
240    }
241}
242
243impl From<Bodies<'_>> for String {
244    /// Convert a [`Bodies`] collection to a [`String`]
245    ///
246    /// # Arguments
247    ///
248    /// * `bodies` - The Bodies collection to convert to a string
249    ///
250    /// # Returns
251    ///
252    /// A string representation of the Bodies with each Body separated by double newlines
253    ///
254    /// # Examples
255    ///
256    /// ```
257    /// use mit_commit::{Bodies, Body};
258    /// let bodies = Bodies::from(vec![
259    ///     Body::from("Body 1"),
260    ///     Body::from("Body 2"),
261    ///     Body::from("Body 3"),
262    /// ]);
263    ///
264    /// assert_eq!(String::from(bodies), "Body 1\n\nBody 2\n\nBody 3");
265    /// ```
266    fn from(bodies: Bodies<'_>) -> Self {
267        bodies
268            .bodies
269            .into_iter()
270            .map(Self::from)
271            .collect::<Vec<_>>()
272            .join("\n\n")
273    }
274}
275
276impl<'a> From<Vec<Fragment<'a>>> for Bodies<'a> {
277    /// Convert a vector of [`Fragment`] to [`Bodies`]
278    ///
279    /// This extracts all Body fragments from the input, skipping the first one (which is typically
280    /// the subject line) and any trailers at the end of the message.
281    ///
282    /// # Arguments
283    ///
284    /// * `bodies` - A vector of Fragment objects to extract Body fragments from
285    ///
286    /// # Returns
287    ///
288    /// A new Bodies instance containing only the Body fragments that are not the subject line
289    /// and not trailers
290    ///
291    /// # Examples
292    ///
293    /// ```
294    /// use mit_commit::{Bodies, Body, Fragment};
295    ///
296    /// let fragments = vec![
297    ///     Fragment::Body(Body::from("Subject Line")),
298    ///     Fragment::Body(Body::default()),
299    ///     Fragment::Body(Body::from("Some content in the body")),
300    ///     Fragment::Body(Body::default()),
301    ///     Fragment::Body(Body::from("Co-authored-by: Someone <someone@example.com>")),
302    /// ];
303    ///
304    /// let bodies = Bodies::from(fragments);
305    ///
306    /// assert_eq!(
307    ///     bodies,
308    ///     Bodies::from(vec![
309    ///         Body::default(),
310    ///         Body::from("Some content in the body"),
311    ///     ])
312    /// );
313    /// ```
314    fn from(bodies: Vec<Fragment<'a>>) -> Self {
315        // Extract all Body fragments
316        let raw_body = bodies
317            .iter()
318            .filter_map(|fragment| match fragment {
319                Fragment::Body(body) => Some(body.clone()),
320                Fragment::Comment(_) => None,
321            })
322            .collect::<Vec<_>>();
323
324        // Count trailers at the end (including empty lines before them)
325        let trailer_count = raw_body
326            .iter()
327            .skip(1)
328            .rev()
329            .take_while(|body| body.is_empty() || Trailer::try_from((*body).clone()).is_ok())
330            .count();
331
332        // Calculate how many non-trailer items to keep, excluding the subject line
333        let non_trailer_item_count = raw_body
334            .len()
335            .saturating_sub(trailer_count)
336            .saturating_sub(1);
337
338        // Extract the body content, skipping subject and trailers
339        raw_body
340            .into_iter()
341            .skip(1) // Skip subject line
342            .take(non_trailer_item_count) // Take only non-trailer content
343            .collect::<Vec<Body<'_>>>()
344            .into()
345    }
346}
347
348#[cfg(test)]
349mod tests {
350    use indoc::indoc;
351
352    use super::Bodies;
353    use crate::{body::Body, fragment::Fragment};
354
355    #[test]
356    fn test_iter_returns_bodies_in_order() -> Result<(), String> {
357        let bodies = Bodies::from(vec![
358            Body::from("Body 1"),
359            Body::from("Body 2"),
360            Body::from("Body 3"),
361        ]);
362        let mut iterator = bodies.iter();
363
364        if iterator.next() != Some(&Body::from("Body 1")) {
365            return Err("First body should be 'Body 1'".to_string());
366        }
367
368        if iterator.next() != Some(&Body::from("Body 2")) {
369            return Err("Second body should be 'Body 2'".to_string());
370        }
371
372        if iterator.next() != Some(&Body::from("Body 3")) {
373            return Err("Third body should be 'Body 3'".to_string());
374        }
375
376        if iterator.next().is_some() {
377            return Err("Iterator should be exhausted after three elements".to_string());
378        }
379
380        Ok(())
381    }
382
383    #[test]
384    fn test_from_bodies_to_string_conversion_formats_correctly() -> Result<(), String> {
385        let bodies = Bodies::from(vec![
386            Body::from("Message Body"),
387            Body::from("Another Message Body"),
388        ]);
389
390        let expected = String::from(indoc!(
391            "
392            Message Body
393
394            Another Message Body"
395        ));
396
397        if String::from(bodies) != expected {
398            return Err(
399                "Bodies should be converted to a string with double newlines between them"
400                    .to_string(),
401            );
402        }
403
404        Ok(())
405    }
406
407    #[test]
408    fn test_display_trait_formats_bodies_correctly() -> Result<(), String> {
409        let bodies = Bodies::from(vec![
410            Body::from("Message Body"),
411            Body::from("Another Message Body"),
412        ]);
413
414        let expected = String::from(indoc!(
415            "
416            Message Body
417
418            Another Message Body"
419        ));
420
421        if format!("{bodies}") != expected {
422            return Err(
423                "Display implementation should format bodies with double newlines between them"
424                    .to_string(),
425            );
426        }
427
428        Ok(())
429    }
430
431    #[test]
432    fn test_first_returns_first_body_when_present() -> Result<(), String> {
433        let bodies = Bodies::from(vec![
434            Body::from("Message Body"),
435            Body::from("Another Message Body"),
436        ]);
437
438        if bodies.first() != Some(Body::from("Message Body")) {
439            return Err("First method should return the first body in the collection".to_string());
440        }
441
442        Ok(())
443    }
444
445    #[test]
446    fn test_from_fragments_extracts_body_content_correctly() {
447        let bodies = Bodies::from(vec![
448            Fragment::Body(Body::from("Subject Line")),
449            Fragment::Body(Body::default()),
450            Fragment::Body(Body::from("Some content in the body of the message")),
451            Fragment::Body(Body::default()),
452            Fragment::Body(Body::from(indoc!(
453                "
454                Co-authored-by: Billie Thomposon <billie@example.com>
455                Co-authored-by: Someone Else <someone@example.com>
456                "
457            ))),
458        ]);
459
460        assert_eq!(
461            bodies,
462            Bodies::from(vec![
463                Body::default(),
464                Body::from("Some content in the body of the message"),
465            ]),
466            "From<Vec<Fragment>> should extract body content, skipping subject and trailers"
467        );
468    }
469}