git_trailers/
lib.rs

1// Copyright © 2019-2020 The Radicle Foundation <hello@radicle.foundation>
2// Copyright © 2021 The Radicle Link Contributors
3//
4// This file is part of radicle-link, distributed under the GPLv3 with Radicle
5// Linking Exception. For full terms see the included LICENSE file.
6
7use std::{borrow::Cow, convert::TryFrom, fmt, ops::Deref};
8
9use thiserror::Error;
10
11#[derive(Debug, Error)]
12#[non_exhaustive]
13pub enum Error {
14    #[error("the trailers paragraph is missing in the given message")]
15    MissingParagraph,
16
17    #[error("trailing data after trailers section: '{0}")]
18    Trailing(String),
19
20    #[error(transparent)]
21    Parse(#[from] nom::Err<nom::error::Error<String>>),
22}
23
24#[derive(Debug, Clone, Eq, PartialEq)]
25pub struct Trailer<'a> {
26    pub token: Token<'a>,
27    pub values: Vec<Cow<'a, str>>,
28}
29
30impl<'a> Trailer<'a> {
31    pub fn display(&'a self, separator: &'a str) -> Display<'a> {
32        Display {
33            trailer: self,
34            separator,
35        }
36    }
37
38    pub fn to_owned(&self) -> OwnedTrailer {
39        OwnedTrailer::from(self)
40    }
41}
42
43#[derive(Debug, Clone, Eq, PartialEq)]
44pub struct Token<'a>(&'a str);
45
46/// A version of the Trailer<'a> which owns it's token and values. Useful for
47/// when you need to carry trailers around in a long lived data structure.
48#[derive(Debug)]
49pub struct OwnedTrailer {
50    pub token: OwnedToken,
51    pub values: Vec<String>,
52}
53
54#[derive(Debug)]
55pub struct OwnedToken(String);
56
57impl<'a> From<&Trailer<'a>> for OwnedTrailer {
58    fn from(t: &Trailer<'a>) -> Self {
59        OwnedTrailer {
60            token: OwnedToken(t.token.0.to_string()),
61            values: t.values.iter().map(|v| v.to_string()).collect(),
62        }
63    }
64}
65
66impl<'a> From<Trailer<'a>> for OwnedTrailer {
67    fn from(t: Trailer<'a>) -> Self {
68        (&t).into()
69    }
70}
71
72impl<'a> From<&'a OwnedTrailer> for Trailer<'a> {
73    fn from(t: &'a OwnedTrailer) -> Self {
74        Trailer {
75            token: Token(t.token.0.as_str()),
76            values: t.values.iter().map(Cow::from).collect(),
77        }
78    }
79}
80
81impl Deref for OwnedToken {
82    type Target = str;
83
84    fn deref(&self) -> &Self::Target {
85        &self.0
86    }
87}
88
89#[derive(Debug, Error)]
90#[non_exhaustive]
91pub enum InvalidToken {
92    #[error("trailing characters: '{0}'")]
93    Trailing(String),
94
95    #[error(transparent)]
96    Parse(#[from] nom::Err<nom::error::Error<String>>),
97}
98
99impl<'a> TryFrom<&'a str> for Token<'a> {
100    type Error = InvalidToken;
101
102    fn try_from(s: &'a str) -> Result<Self, Self::Error> {
103        match parser::token(s) {
104            Ok((rest, token)) if rest.is_empty() => Ok(token),
105            Ok((trailing, _)) => Err(InvalidToken::Trailing(trailing.to_owned())),
106            Err(e) => Err(e.to_owned().into()),
107        }
108    }
109}
110
111impl Deref for Token<'_> {
112    type Target = str;
113
114    fn deref(&self) -> &Self::Target {
115        self.0
116    }
117}
118
119pub struct Display<'a> {
120    trailer: &'a Trailer<'a>,
121    separator: &'a str,
122}
123
124impl<'a> fmt::Display for Display<'a> {
125    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
126        write!(
127            f,
128            "{}{}{}",
129            self.trailer.token.deref(),
130            self.separator,
131            self.trailer.values.join("\n  ")
132        )
133    }
134}
135
136pub trait Separator<'a> {
137    fn sep_for(&self, token: &Token) -> &'a str;
138}
139
140impl<'a> Separator<'a> for &'a str {
141    fn sep_for(&self, _: &Token) -> &'a str {
142        self
143    }
144}
145
146impl<'a, F> Separator<'a> for F
147where
148    F: Fn(&Token) -> &'a str,
149{
150    fn sep_for(&self, token: &Token) -> &'a str {
151        self(token)
152    }
153}
154
155pub struct DisplayMany<'a, S> {
156    separator: S,
157    trailers: &'a [Trailer<'a>],
158}
159
160impl<'a, S> fmt::Display for DisplayMany<'a, S>
161where
162    S: Separator<'a>,
163{
164    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
165        for (i, trailer) in self.trailers.iter().enumerate() {
166            if i > 0 {
167                writeln!(f)?
168            }
169
170            write!(
171                f,
172                "{}",
173                trailer.display(self.separator.sep_for(&trailer.token))
174            )?
175        }
176
177        Ok(())
178    }
179}
180
181/// Parse the trailers of the given message. It looks up the last paragraph
182/// of the message and attempts to parse each of its lines as a [Trailer].
183/// Fails if no trailers paragraph is found or if at least one trailer
184/// fails to be parsed.
185pub fn parse<'a>(message: &'a str, separators: &'a str) -> Result<Vec<Trailer<'a>>, Error> {
186    let trailers_paragraph =
187        match parser::paragraphs(message.trim_end()).map(|(_, ps)| ps.last().cloned()) {
188            Ok(None) | Err(_) => return Err(Error::MissingParagraph),
189            Ok(Some(p)) => {
190                if p.is_empty() {
191                    return Err(Error::MissingParagraph);
192                }
193                p
194            },
195        };
196
197    match parser::trailers(trailers_paragraph, separators) {
198        Ok((rest, trailers)) if rest.is_empty() => Ok(trailers),
199        Ok((unparseable, _)) => Err(Error::Trailing(unparseable.to_owned())),
200        Err(e) => Err(e.to_owned().into()),
201    }
202}
203
204/// Render a slice of trailers.
205///
206/// The `separator` can be either a string slice, or a closure which may choose
207/// a different separator for each [`Token`] encountered. Note that multiline
208/// trailers are rendered with a fixed indent, so the result is not
209/// layout-preserving.
210pub fn display<'a, S>(separator: S, trailers: &'a [Trailer<'a>]) -> DisplayMany<'a, S>
211where
212    S: Separator<'a>,
213{
214    DisplayMany {
215        separator,
216        trailers,
217    }
218}
219
220pub mod parser {
221    use std::borrow::Cow;
222
223    use super::{Token, Trailer};
224    use nom::{
225        branch::alt,
226        bytes::complete::{tag, take_until, take_while1},
227        character::complete::{line_ending, not_line_ending, one_of, space0, space1},
228        combinator::{map, rest},
229        multi::{many0, separated_list1},
230        sequence::{delimited, preceded, separated_pair, terminated},
231        IResult,
232    };
233
234    const EMPTY_LINE: &str = "\n\n";
235
236    pub fn paragraphs(s: &str) -> IResult<&str, Vec<&str>> {
237        separated_list1(tag(EMPTY_LINE), paragraph)(s)
238    }
239
240    pub fn paragraph(s: &str) -> IResult<&str, &str> {
241        alt((take_until(EMPTY_LINE), rest))(s)
242    }
243
244    /// Parse all the possible trailers.
245    /// It stops when it can no longer parse valid trailers.
246    pub fn trailers<'a>(s: &'a str, separators: &'a str) -> IResult<&'a str, Vec<Trailer<'a>>> {
247        many0(|s| trailer(s, separators))(s)
248    }
249
250    /// Parse a trailer, which can have an inlined or multilined value.
251    pub fn trailer<'a>(s: &'a str, separators: &'a str) -> IResult<&'a str, Trailer<'a>> {
252        let mut parser = separated_pair(token, |s| separator(separators, s), values);
253        let (rest, (token, values)) = parser(s)?;
254        Ok((rest, Trailer { token, values }))
255    }
256
257    /// Parse a trailer token.
258    pub(super) fn token(s: &str) -> IResult<&str, Token> {
259        take_while1(|c: char| c.is_alphanumeric() || c == '-')(s)
260            .map(|(i, token_str)| (i, Token(token_str)))
261    }
262
263    /// Parse the trailer separator, which can be delimited by spaces.
264    fn separator<'a>(separators: &'a str, s: &'a str) -> IResult<&'a str, char> {
265        delimited(space0, one_of(separators), space0)(s)
266    }
267
268    /// Parse the trailer values, which gathers the value after the separator
269    /// (if any) and possible following multilined values, indented by a
270    /// space.
271    fn values(s: &str) -> IResult<&str, Vec<Cow<'_, str>>> {
272        let (r, opt_inline_value) = until_eol_or_eof(s)?;
273        let (r, mut values) = multiline_values(r)?;
274        if !opt_inline_value.is_empty() {
275            values.insert(0, opt_inline_value.into())
276        }
277        Ok((r, values))
278    }
279
280    fn multiline_values(s: &str) -> IResult<&str, Vec<Cow<'_, str>>> {
281        many0(map(indented_line_contents, Cow::from))(s)
282    }
283
284    fn until_eol_or_eof(s: &str) -> IResult<&str, &str> {
285        alt((until_eol, rest))(s)
286    }
287
288    /// Parse an indented line, i.e, a line that starts with a space.
289    /// Extracts the line contents, ignoring the indentation and the
290    /// new line character.
291    fn indented_line_contents(s: &str) -> IResult<&str, &str> {
292        preceded(space1, until_eol_or_eof)(s)
293    }
294
295    /// Consume the input until the end of the line, ignoring the new line
296    /// character.
297    fn until_eol(s: &str) -> IResult<&str, &str> {
298        terminated(not_line_ending, line_ending)(s)
299    }
300}