aboutsummaryrefslogtreecommitdiff
path: root/libgitmail/src/patch/parsers.rs
blob: 9779e2b48b08ec17c2cf5832b3f90fb940e7949b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
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
    }
  );
}