1use serde::{Deserialize, Serialize};
2use std::fmt::Display;
3use std::str::FromStr;
4use winnow::Result;
5use winnow::ascii::space0;
6use winnow::combinator::opt;
7use winnow::combinator::separated;
8use winnow::error::ContextError;
9use winnow::prelude::*;
10use winnow::stream::Accumulate;
11use winnow::token::take_till;
12
13#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)]
14pub struct Author {
15 name: String,
16 email: Option<String>,
17}
18
19impl Author {
20 pub fn new(name: String, email: Option<String>) -> Self {
21 Self { name, email }
22 }
23
24 pub fn name(&self) -> &str {
25 &self.name
26 }
27
28 pub fn email(&self) -> Option<&String> {
29 self.email.as_ref()
30 }
31}
32
33impl Display for Author {
34 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35 write!(f, "{}", self.name)?;
36 if let Some(email) = &self.email {
37 write!(f, " <{email}>")?;
38 }
39 Ok(())
40 }
41}
42
43fn separator(s: &mut &str) -> Result<()> {
44 let _ = space0.parse_next(s)?;
45 let _ = ",".parse_next(s)?;
46 let _ = space0.parse_next(s)?;
47 Ok(())
48}
49
50fn name(s: &mut &str) -> Result<String> {
51 let name = take_till(1.., |c| matches!(c, ']' | '<' | ',' | '"')).parse_next(s)?;
52 let name = name.trim().to_string();
53 Ok(name)
54}
55
56fn email(s: &mut &str) -> Result<String> {
57 let _ = "<".parse_next(s)?;
58 let email = take_till(1.., |c| c == '>')
59 .map(|x: &str| x.to_string())
60 .parse_next(s)?;
61 let _ = ">".parse_next(s)?;
62 Ok(email)
63}
64
65fn author(s: &mut &str) -> Result<Author> {
66 let _ = opt("\"").parse_next(s)?;
67 let name = name.parse_next(s)?;
68 let email = opt(email).parse_next(s)?;
69 let _ = opt("\"").parse_next(s)?;
70 Ok(Author { name, email })
71}
72
73#[derive(Debug, Eq, PartialEq)]
74pub struct ParseError {
75 message: String,
76 span: std::ops::Range<usize>,
77 input: String,
78}
79
80impl ParseError {
81 fn from_parse(error: winnow::error::ParseError<&str, ContextError>) -> Self {
82 let message = error.inner().to_string();
83 let input = (*error.input()).to_owned();
84 let span = error.char_span();
85 Self {
86 message,
87 span,
88 input,
89 }
90 }
91}
92
93impl std::fmt::Display for ParseError {
94 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
95 let message = annotate_snippets::Level::Error
96 .title(&self.message)
97 .snippet(
98 annotate_snippets::Snippet::source(&self.input)
99 .fold(true)
100 .annotation(annotate_snippets::Level::Error.span(self.span.clone())),
101 );
102 let renderer = annotate_snippets::Renderer::plain();
103 let rendered = renderer.render(message);
104 rendered.fmt(f)
105 }
106}
107
108impl std::error::Error for ParseError {}
109
110impl FromStr for Authors {
111 type Err = ParseError;
112 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
113 authors.parse(input).map_err(|e| ParseError::from_parse(e))
114 }
115}
116
117impl FromStr for Author {
118 type Err = ParseError;
119 fn from_str(input: &str) -> std::result::Result<Self, Self::Err> {
120 author.parse(input).map_err(|e| ParseError::from_parse(e))
121 }
122}
123
124#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone)]
125pub struct Authors {
126 authors: Vec<Author>,
127}
128
129impl Authors {
130 pub fn len(&self) -> usize {
131 self.authors.len()
132 }
133
134 pub fn is_empty(&self) -> bool {
135 self.authors.is_empty()
136 }
137}
138
139impl<'a> IntoIterator for &'a Authors {
140 type Item = &'a Author;
141 type IntoIter = std::slice::Iter<'a, Author>;
142 fn into_iter(self) -> Self::IntoIter {
143 self.authors.iter()
144 }
145}
146
147impl<'a> IntoIterator for &'a mut Authors {
148 type Item = &'a mut Author;
149 type IntoIter = std::slice::IterMut<'a, Author>;
150 fn into_iter(self) -> Self::IntoIter {
151 self.authors.iter_mut()
152 }
153}
154
155impl Accumulate<Author> for Authors {
156 fn initial(capacity: Option<usize>) -> Self {
157 let authors = match capacity {
158 Some(capacity) => Vec::with_capacity(capacity),
159 None => Vec::new(),
160 };
161 Authors { authors }
162 }
163
164 fn accumulate(&mut self, acc: Author) {
165 self.authors.push(acc);
166 }
167}
168
169fn authors(s: &mut &str) -> winnow::Result<Authors> {
170 let _ = opt("[").parse_next(s)?;
171 let authors = separated(1.., author, separator).parse_next(s)?;
172 let _ = opt("]").parse_next(s)?;
173 Ok(authors)
174}
175
176#[cfg(test)]
177#[allow(clippy::declare_interior_mutable_const)]
178mod tests {
179 use super::*;
180 use pretty_assertions::assert_eq;
181 use rstest::rstest;
182 use s_string::s;
183 use std::cell::LazyCell;
184
185 const FOOBAR: LazyCell<Authors> = LazyCell::new(|| Authors {
186 authors: vec![Author {
187 name: s!("Foo Bar"),
188 email: Some(s!("foo@bar.com")),
189 }],
190 });
191
192 const FOOBAR_NO_EMAIL: LazyCell<Authors> = LazyCell::new(|| Authors {
193 authors: vec![Author {
194 name: s!("Foo Bar"),
195 email: None,
196 }],
197 });
198
199 const FOOBAR_AUTHOR: LazyCell<Author> = LazyCell::new(|| Author {
200 name: s!("Foo Bar"),
201 email: Some(s!("foo@bar.com")),
202 });
203
204 const FOOBAR_NO_EMAIL_AUTHOR: LazyCell<Author> = LazyCell::new(|| Author {
205 name: s!("Foo Bar"),
206 email: None,
207 });
208
209 const MULTIPLE: LazyCell<Authors> = LazyCell::new(|| Authors {
210 authors: vec![
211 Author {
212 name: s!("Foo Bar"),
213 email: Some(s!("foo@bar.com")),
214 },
215 Author {
216 name: s!("Foo2 Bar"),
217 email: Some(s!("foo2@bar.com")),
218 },
219 Author {
220 name: s!("Foo3 Bar"),
221 email: Some(s!("foo3@bar.com")),
222 },
223 ],
224 });
225
226 #[test]
227 fn test_parse_email() {
228 let mut input = "<firstlast@foo.com>";
229 let expected = s!("firstlast@foo.com");
230 let actual = email.parse_next(&mut input);
231 assert_eq!(Ok(expected), actual);
232 }
233
234 #[test]
235 fn test_parse_author() {
236 let mut input = "First Last <firstlast@foo.com>";
237 let expected = Author {
238 name: s!("First Last"),
239 email: Some(s!("firstlast@foo.com")),
240 };
241 let actual = author.parse_next(&mut input);
242 assert_eq!(Ok(expected), actual);
243 }
244
245 #[rstest]
246 #[case("Foo Bar <foo@bar.com>", FOOBAR)]
247 #[case("[Foo Bar <foo@bar.com>]", FOOBAR)]
248 #[case("\"Foo Bar <foo@bar.com>\"", FOOBAR)]
249 #[case("[\"Foo Bar <foo@bar.com>\"]", FOOBAR)]
250 #[case("Foo Bar", FOOBAR_NO_EMAIL)]
251 #[case("[Foo Bar]", FOOBAR_NO_EMAIL)]
252 #[case("\"Foo Bar\"", FOOBAR_NO_EMAIL)]
253 #[case("[\"Foo Bar\"]", FOOBAR_NO_EMAIL)]
254 fn test_single_authors(#[case] input: &str, #[case] expected: LazyCell<Authors>) {
255 let actual = Authors::from_str(input);
256 assert_eq!(Ok((*expected).clone()), actual);
257 }
258
259 #[rstest]
260 #[case(
261 "[\"Foo Bar <foo@bar.com>\", \"Foo2 Bar <foo2@bar.com>\", \"Foo3 Bar <foo3@bar.com>\"]",
262 MULTIPLE
263 )]
264 #[case(
265 "[Foo Bar <foo@bar.com>, Foo2 Bar <foo2@bar.com>, Foo3 Bar <foo3@bar.com>]",
266 MULTIPLE
267 )]
268 fn test_multiple_authors(#[case] input: &str, #[case] expected: LazyCell<Authors>) {
269 let actual = Authors::from_str(input);
270 assert_eq!(Ok((*expected).clone()), actual);
271 }
272
273 #[rstest]
274 #[case("Foo Bar <foo@bar.com>", FOOBAR_AUTHOR)]
275 #[case("\"Foo Bar <foo@bar.com>\"", FOOBAR_AUTHOR)]
276 #[case("Foo Bar", FOOBAR_NO_EMAIL_AUTHOR)]
277 #[case("\"Foo Bar\"", FOOBAR_NO_EMAIL_AUTHOR)]
278 fn test_author(#[case] input: &str, #[case] expected: LazyCell<Author>) {
279 let actual = Author::from_str(input);
280 assert_eq!(Ok((*expected).clone()), actual);
281 }
282}