//! 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, /// 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 { 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::().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::().unwrap_or(1); let this = this_opt.unwrap().parse::().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
{ headers.get(key).map_or(Err(Error::FailedParsing), |h| { match h.split(",").map(|s| s.trim()).collect::>() { 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 } ); }