diff options
Diffstat (limited to 'libgitmail/src/patch/parsers.rs')
-rw-r--r-- | libgitmail/src/patch/parsers.rs | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/libgitmail/src/patch/parsers.rs b/libgitmail/src/patch/parsers.rs new file mode 100644 index 0000000..9779e2b --- /dev/null +++ b/libgitmail/src/patch/parsers.rs @@ -0,0 +1,163 @@ +//! 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 + } + ); +} |