aboutsummaryrefslogtreecommitdiff
path: root/libgitmail/src/patch/parsers.rs
//! Contains additional parsers that are required for git patch emails
//!
//! The parsers are written in nom, and generate
//! [Patch](../struct.Patch.html) objects, or an Error.  A parser
//! should neven panic.
//!
//! There's also some unit and quick check tests that make sure that
//! valid git patch emails can be parsed.  If you have emails you
//! don't mind becoming part of the test set, feel free to email in a
//! patch!

use crate::{
  patch::{Header, HeaderMap},
  Error, Result,
};
use nom::{
  bytes::complete::{is_not, tag, take_till},
  character::complete::char,
  combinator::opt,
  sequence::{delimited, preceded},
  IResult,
};

/// The segment data for a patch mail
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct Segment {
  /// The number of a patch in it's series
  pub this: u8,
  /// The total number of patches in a seriesThe first
  pub max: u8,
}

/// The typed subject line of a patch mail
///
/// A message might be: "[PATCH v2 1/5], which is parsed into this
/// structure to be easier to work with
#[derive(Clone, Default, Debug, PartialEq, Eq)]
pub struct Subject {
  /// The patch revision (e.g. `v2`)
  pub version: u8,
  /// Patch segment information (e.g. `1/5`)
  pub segment: Option<Segment>,
  /// Subject prefix (e.g. `PATCH`)
  pub prefix: String,
  /// The actual patch commit message
  pub message: String,
}

impl Subject {
  /// Create a typed subject from a subject string
  pub fn from(s: String) -> Result<Self> {
    Ok(Self::parse(s.as_str()).unwrap().1)
  }

  /// This is a terrible function that does terrible things
  fn parse(input: &str) -> IResult<&str, Self> {
    let input = opt(tag("Re:"))(input).map(|(i, _)| i.trim())?;
    let (msg, input) = Self::parens(input).map(|(a, b)| (a.trim(), b))?;
    let (input, prefix) =
      take_till(|c| c == ' ')(input).map(|(a, b)| (a.trim(), b))?;

    let (input, version) =
      opt(preceded(tag("v"), take_till(|c| c == ' ')))(input)
        .map(|(i, v)| (i.trim(), v.map_or(1, |v| v.parse::<u8>().unwrap())))?;

    let (max_str, this_opt) = opt(take_till(|c| c == '/'))(input)?;

    let segment = if max_str.len() > 1 {
      let max = max_str[1..].parse::<u8>().unwrap_or(1);
      let this = this_opt.unwrap().parse::<u8>().unwrap();
      Some(Segment { max, this })
    } else {
      None
    };

    Ok((
      input,
      Self {
        message: msg.into(),
        prefix: prefix.into(),
        version,
        segment,
      },
    ))
  }

  /// Grab text between the [ ... ] section of a subject
  fn parens(input: &str) -> IResult<&str, &str> {
    delimited(char('['), is_not("]"), char(']'))(input)
  }
}

/// A utility function that can get a header into a semi-typed field
pub(crate) fn get_header(headers: &HeaderMap, key: &str) -> Result<Header> {
  headers.get(key).map_or(Err(Error::FailedParsing), |h| {
    match h.split(",").map(|s| s.trim()).collect::<Vec<_>>() {
      vec if vec.len() == 0 => Err(Error::FailedParsing),
      vec if vec.len() == 1 => {
        Ok(Header::Single(String::from(*vec.get(0).unwrap())))
      }
      vec => Ok(Header::Multi(
        vec.into_iter().map(|s| String::from(s)).collect(),
      )),
    }
  })
}

#[test]
fn full_test() {
  let subject =
    "Re: [PATCH v2 2/3] docs: fix spelling of \"contributors\"".into();
  assert_eq!(
    Subject::from(subject).unwrap(),
    Subject {
      message: "docs: fix spelling of \"contributors\"".into(),
      prefix: "PATCH".into(),
      version: 2,
      segment: Some(Segment { max: 3, this: 2 })
    }
  );
}

#[test]
fn minimal_test() {
  let subject = "Re: [PATCH] docs: fix spelling of \"contributors\"".into();
  assert_eq!(
    Subject::from(subject).unwrap(),
    Subject {
      message: "docs: fix spelling of \"contributors\"".into(),
      prefix: "PATCH".into(),
      version: 1,
      segment: None
    }
  );
}

#[test]
fn version_test() {
  let subject = "Re: [PATCH v4] docs: fix spelling of \"contributors\"".into();
  assert_eq!(
    Subject::from(subject).unwrap(),
    Subject {
      message: "docs: fix spelling of \"contributors\"".into(),
      prefix: "PATCH".into(),
      version: 4,
      segment: None
    }
  );
}

#[test]
fn double_digit_version_test() {
  let subject = "Re: [PATCH v11] docs: fix spelling of \"contributors\"".into();
  assert_eq!(
    Subject::from(subject).unwrap(),
    Subject {
      message: "docs: fix spelling of \"contributors\"".into(),
      prefix: "PATCH".into(),
      version: 11,
      segment: None
    }
  );
}