authors/
lib.rs

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}