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}