use std::{
convert::TryFrom,
fmt,
fmt::{Display, Formatter},
slice::Iter,
vec::IntoIter,
};
use crate::{body::Body, fragment::Fragment, trailer::Trailer};
#[derive(Debug, PartialEq, Eq, Clone, Default)]
pub struct Bodies<'a> {
bodies: Vec<Body<'a>>,
}
impl Bodies<'_> {
#[must_use]
pub fn first(&self) -> Option<Body<'_>> {
self.bodies.first().cloned()
}
pub fn iter(&self) -> Iter<'_, Body<'_>> {
self.bodies.iter()
}
}
impl<'a> IntoIterator for Bodies<'a> {
type Item = Body<'a>;
type IntoIter = IntoIter<Body<'a>>;
fn into_iter(self) -> Self::IntoIter {
self.bodies.into_iter()
}
}
impl<'a> IntoIterator for &'a Bodies<'a> {
type IntoIter = Iter<'a, Body<'a>>;
type Item = &'a Body<'a>;
fn into_iter(self) -> Self::IntoIter {
self.bodies.iter()
}
}
impl Display for Bodies<'_> {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
if self.bodies.is_empty() {
return Ok(());
}
let mut iter = self.bodies.iter();
if let Some(first) = iter.next() {
write!(f, "{first}")?;
for body in iter {
write!(f, "\n\n{body}")?;
}
}
Ok(())
}
}
impl<'a> From<Vec<Body<'a>>> for Bodies<'a> {
fn from(bodies: Vec<Body<'a>>) -> Self {
Self { bodies }
}
}
impl From<Bodies<'_>> for String {
fn from(bodies: Bodies<'_>) -> Self {
bodies
.bodies
.into_iter()
.map(Self::from)
.collect::<Vec<_>>()
.join("\n\n")
}
}
impl<'a> From<Vec<Fragment<'a>>> for Bodies<'a> {
fn from(bodies: Vec<Fragment<'a>>) -> Self {
let raw_body = bodies
.iter()
.filter_map(|fragment| match fragment {
Fragment::Body(body) => Some(body.clone()),
Fragment::Comment(_) => None,
})
.collect::<Vec<_>>();
let trailer_count = raw_body
.iter()
.skip(1)
.rev()
.take_while(|body| body.is_empty() || Trailer::try_from((*body).clone()).is_ok())
.count();
let non_trailer_item_count = raw_body
.len()
.saturating_sub(trailer_count)
.saturating_sub(1);
raw_body
.into_iter()
.skip(1) .take(non_trailer_item_count) .collect::<Vec<Body<'_>>>()
.into()
}
}
#[cfg(test)]
mod tests {
use indoc::indoc;
use super::Bodies;
use crate::{body::Body, fragment::Fragment};
#[test]
fn test_iter_returns_bodies_in_order() -> Result<(), String> {
let bodies = Bodies::from(vec![
Body::from("Body 1"),
Body::from("Body 2"),
Body::from("Body 3"),
]);
let mut iterator = bodies.iter();
if iterator.next() != Some(&Body::from("Body 1")) {
return Err("First body should be 'Body 1'".to_string());
}
if iterator.next() != Some(&Body::from("Body 2")) {
return Err("Second body should be 'Body 2'".to_string());
}
if iterator.next() != Some(&Body::from("Body 3")) {
return Err("Third body should be 'Body 3'".to_string());
}
if iterator.next().is_some() {
return Err("Iterator should be exhausted after three elements".to_string());
}
Ok(())
}
#[test]
fn test_from_bodies_to_string_conversion_formats_correctly() -> Result<(), String> {
let bodies = Bodies::from(vec![
Body::from("Message Body"),
Body::from("Another Message Body"),
]);
let expected = String::from(indoc!(
"
Message Body
Another Message Body"
));
if String::from(bodies) != expected {
return Err(
"Bodies should be converted to a string with double newlines between them"
.to_string(),
);
}
Ok(())
}
#[test]
fn test_display_trait_formats_bodies_correctly() -> Result<(), String> {
let bodies = Bodies::from(vec![
Body::from("Message Body"),
Body::from("Another Message Body"),
]);
let expected = String::from(indoc!(
"
Message Body
Another Message Body"
));
if format!("{bodies}") != expected {
return Err(
"Display implementation should format bodies with double newlines between them"
.to_string(),
);
}
Ok(())
}
#[test]
fn test_first_returns_first_body_when_present() -> Result<(), String> {
let bodies = Bodies::from(vec![
Body::from("Message Body"),
Body::from("Another Message Body"),
]);
if bodies.first() != Some(Body::from("Message Body")) {
return Err("First method should return the first body in the collection".to_string());
}
Ok(())
}
#[test]
fn test_from_fragments_extracts_body_content_correctly() {
let bodies = Bodies::from(vec![
Fragment::Body(Body::from("Subject Line")),
Fragment::Body(Body::default()),
Fragment::Body(Body::from("Some content in the body of the message")),
Fragment::Body(Body::default()),
Fragment::Body(Body::from(indoc!(
"
Co-authored-by: Billie Thomposon <billie@example.com>
Co-authored-by: Someone Else <someone@example.com>
"
))),
]);
assert_eq!(
bodies,
Bodies::from(vec![
Body::default(),
Body::from("Some content in the body of the message"),
]),
"From<Vec<Fragment>> should extract body content, skipping subject and trailers"
);
}
}