1use std::convert::{identity, TryFrom};
2
3use nom::{
4 bytes::complete::{is_not, tag, tag_no_case, take_until},
5 character::complete::space0,
6 combinator::{map, opt, verify},
7 error::ErrorKind,
8 multi::many1,
9 sequence::{delimited, preceded},
10 Err, IResult,
11};
12
13#[derive(Copy, Clone, Debug, PartialEq, Eq)]
27pub struct CoAuthor<'a> {
28 pub name: &'a str,
29 pub mail: Option<&'a str>,
30}
31
32impl<'a> TryFrom<&'a str> for CoAuthor<'a> {
33 type Error = CoAuthorError;
34
35 fn try_from(line: &'a str) -> Result<Self, Self::Error> {
36 match co_author(line) {
37 Ok((_, (name, mail))) => Ok(CoAuthor { name, mail }),
38 Err(e) => match e {
39 Err::Incomplete(_) => Err(CoAuthorError::MissingTrailerKey),
40 Err::Error(e) | Err::Failure(e) => match e.code {
41 ErrorKind::Tag => Err(CoAuthorError::MissingTrailerKey),
42 ErrorKind::TakeUntil => Err(CoAuthorError::MissingMail),
43 ErrorKind::Verify => Err(CoAuthorError::MissingName),
44 otherwise => unreachable!("Unexpected error kind: {:?}", otherwise),
45 },
46 },
47 }
48 }
49}
50
51#[derive(Copy, Clone, Debug, PartialEq, Eq)]
53pub enum CoAuthorError {
54 MissingTrailerKey,
56 MissingName,
58 MissingMail,
60}
61
62impl std::fmt::Display for CoAuthorError {
63 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64 match self {
65 CoAuthorError::MissingTrailerKey => {
66 f.write_str("The trailer is missing the `Co-Authored-By:` key.")
67 }
68 CoAuthorError::MissingName => f.write_str("The name of the co-author is missing."),
69 CoAuthorError::MissingMail => f.write_str("The mail of the co-author is missing."),
70 }
71 }
72}
73
74impl std::error::Error for CoAuthorError {}
75
76#[deprecated(since = "0.1.0", note = "Use `CoAuthor::try_from` instead")]
77pub fn get_co_author(line: &str) -> Option<CoAuthor> {
78 let (_, (name, mail)) = co_author(line).ok()?;
79 Some(CoAuthor { name, mail })
80}
81
82fn co_author(input: &str) -> IResult<&str, (&str, Option<&str>)> {
83 let (input, name) = co_author_name(input)?;
84 let (input, email) = co_author_mail(input)?;
85 Ok((input, (name, email)))
86}
87
88fn co_author_name(input: &str) -> IResult<&str, &str> {
89 let co_author_name = take_until("<");
90 let co_author_name = map(co_author_name, str::trim);
91 let co_author_name = verify(co_author_name, |s: &str| !s.is_empty());
92 let co_author_name = preceded(co_authored_by, co_author_name);
93 identity(co_author_name)(input)
94}
95
96fn co_authored_by(input: &str) -> IResult<&str, Vec<()>> {
97 let co_authored_by = tag_no_case("co-authored-by:");
98 let co_authored_by = map(co_authored_by, |_| ());
99 let co_authored_by = delimited(space0, co_authored_by, space0);
100 let co_authored_by = many1(co_authored_by);
101 identity(co_authored_by)(input)
102}
103
104fn co_author_mail(input: &str) -> IResult<&str, Option<&str>> {
105 let co_author_mail = is_not("> \t");
106 let co_author_mail = delimited(tag("<"), co_author_mail, tag(">"));
107 let co_author_mail = opt(co_author_mail);
108 identity(co_author_mail)(input)
109}
110
111#[cfg(test)]
112mod tests {
113 use test_case::test_case;
114
115 use super::*;
116
117 #[test_case("co-authored-by: Alice <alice@wonderland.org>", "Alice <alice@wonderland.org>"; "lower case")]
118 #[test_case("Co-Authored-By: Alice <alice@wonderland.org>", "Alice <alice@wonderland.org>"; "camel case")]
119 #[test_case("CO-AUTHORED-BY: Alice <alice@wonderland.org>", "Alice <alice@wonderland.org>"; "upper case")]
120 #[test_case("Co-authored-by: Alice <alice@wonderland.org>", "Alice <alice@wonderland.org>"; "mixed case")]
121 #[test_case("Co-authored-by: Co-authored-by: Alice <alice@wonderland.org>", "Alice <alice@wonderland.org>"; "florentin case")]
122 fn test_co_authored_by(input: &str, expected: &str) {
123 let (result, _) = co_authored_by(input).unwrap();
124 assert_eq!(result, expected)
125 }
126
127 #[test_case("co-authored-by: Alice <alice@wonderland.org>", "Alice"; "alice")]
128 #[test_case("co-authored-by: Alice Bob <alice@wonderland.org>", "Alice Bob"; "alice bob")]
129 fn test_co_author_name(input: &str, expected: &str) {
130 let (_, result) = co_author_name(input).unwrap();
131 assert_eq!(result, expected)
132 }
133
134 #[test_case("<alice@wonderland.org>", "alice@wonderland.org"; "alice")]
135 #[test_case("<alice@wonderland.org> bob", "alice@wonderland.org"; "alice bob")]
136 #[test_case("<alice@wonderland.org> <charlie@wonderland.org>", "alice@wonderland.org"; "alice charlie")]
137 fn test_co_author_mail(input: &str, expected: &str) {
138 let (_, result) = co_author_mail(input).unwrap();
139 assert_eq!(result.unwrap(), expected)
140 }
141
142 #[test_case(""; "empty")]
143 #[test_case(" <alice@wonderland.org>"; "leading space")]
144 #[test_case("<alice@wonderland.org"; "missing close")]
145 #[test_case("<alice@wonderland.org>"; "encoded close")]
146 #[test_case("alice@wonderland.org>"; "missing open")]
147 #[test_case("<alice and bob@wonderland.org>"; "contains whitespace")]
148 fn test_missing_co_author_mail(input: &str) {
149 let (_, result) = co_author_mail(input).unwrap();
150 assert_eq!(result, None)
151 }
152
153 #[test_case("co-authored-by: Alice <alice@wonderland.org>" => Some("Alice"); "alice")]
154 #[test_case("co-authored-by: Alice Keys <alice@wonderland.org>" => Some("Alice Keys"); "alice keys")]
155 #[test_case("Co-Authored-By:Alice<alice@wonderland.org>" => Some("Alice"); "no space alice")]
156 #[test_case("Some other content" => None; "none")]
157 fn test_get_co_author_name(input: &str) -> Option<&str> {
158 CoAuthor::try_from(input)
159 .ok()
160 .map(|co_author| co_author.name)
161 }
162
163 #[test_case("co-authored-by: Alice <alice@wonderland.org>" => Some("alice@wonderland.org"); "alice")]
164 #[test_case("co-authored-by: Alice Keys <alice@wonderland.org>" => Some("alice@wonderland.org"); "alice keys")]
165 #[test_case("co-authored-by: <alice@wonderland.org>" => None; "missing name")]
166 #[test_case("Some other content" => None; "none")]
167 fn test_get_co_author_mail(input: &str) -> Option<&str> {
168 CoAuthor::try_from(input)
169 .ok()
170 .and_then(|co_author| co_author.mail)
171 }
172
173 #[test_case("Alice <alice@wonderland.org>"; "missing")]
174 #[test_case("co-authored-by Alice <alice@wonderland.org>"; "missing colon")]
175 #[test_case("Co-Authored: Alice <alice@wonderland.org>"; "missing By")]
176 #[test_case("Authored-By: Alice <alice@wonderland.org>"; "missing Co")]
177 #[test_case(""; "empty input")]
178 fn test_missing_trailer_key(input: &str) {
179 let err = CoAuthor::try_from(input).unwrap_err();
180 assert_eq!(err, CoAuthorError::MissingTrailerKey)
181 }
182
183 #[test_case("Co-Authored-By: <alice@wonderland.org>"; "missing name")]
184 fn test_missing_name(input: &str) {
185 let err = CoAuthor::try_from(input).unwrap_err();
186 assert_eq!(err, CoAuthorError::MissingName)
187 }
188
189 #[test_case("Co-Authored-By: Alice"; "missing mail")]
190 fn test_missing_mail(input: &str) {
191 let err = CoAuthor::try_from(input).unwrap_err();
192 assert_eq!(err, CoAuthorError::MissingMail)
193 }
194}