Skip to main content

alpm_parsers/error/
context_parser.rs

1//! A [`ContextParser`] that attaches context messages to a parser.
2//!
3//! This is our own `String`-capable version winnow's [`Context`] parser and its respective
4//! [`ContextExt`] trait to expose functions for adding context on all parsers.
5//!
6//! [`Context`]: winnow::combinator::impls::Context
7
8use std::borrow::Borrow;
9
10use winnow::{Parser, error::ErrMode};
11
12use super::{Input, PResult, ParseStack, context::StringContext};
13
14/// A parser that attaches context to the [`ParseStack`] in case of an error.
15///
16/// This is our equivalent to winnow's [`Context`] parser.
17///
18/// [`Context`]: winnow::combinator::impls::Context
19#[derive(Debug)]
20pub struct ContextParser<P> {
21    pub(crate) parser: P,
22    pub(crate) contexts: Vec<StringContext>,
23}
24
25impl<'i, O, P> Parser<Input<'i>, O, ErrMode<ParseStack<'i>>> for ContextParser<P>
26where
27    P: Parser<Input<'i>, O, ErrMode<ParseStack<'i>>>,
28{
29    #[inline]
30    fn parse_next(&mut self, i: &mut Input<'i>) -> PResult<'i, O> {
31        self.parser.parse_next(i).map_err(|e| {
32            e.map(|mut stack| {
33                stack.pending.extend(self.contexts.iter().cloned());
34                stack
35            })
36        })
37    }
38}
39
40/// The trait that adds context functions to all parsers over [`Input`].
41///
42/// These functions are the [`String`]-capable counterparts to [`Parser::context`].
43///
44/// # Note
45///
46/// Just like [`Parser::context`] calls, any attached context becomes part of the **current** layer,
47/// which is closed by the **next** [`LayerExt::layer`].
48///
49/// # Example
50///
51/// ```rust
52/// use alpm_parsers::prelude::*;
53/// use winnow::ascii::digit1;
54///
55/// # fn main() -> testresult::TestResult {
56/// // Context messages may be built at runtime.
57/// let expected = format!("Hoped for exactly {} decimal digit", 1);
58/// let mut parser = digit1
59///     .label("version number")
60///     .description(expected)
61///     .layer("version");
62///
63/// parser.parse(Input::new("42"))?;
64/// # Ok(())
65/// # }
66/// ```
67///
68/// [`Parser::context`]: winnow::Parser::context
69/// [`LayerExt::layer`]: crate::error::LayerExt::layer
70pub trait ContextExt<'i, O>: Parser<Input<'i>, O, ErrMode<ParseStack<'i>>> + Sized {
71    /// Set a label that describes what is currently being parsed.
72    fn label(self, label: impl Into<String>) -> ContextParser<Self> {
73        ContextParser {
74            parser: self,
75            contexts: vec![StringContext::Label(label.into())],
76        }
77    }
78
79    /// Add a `char` literal to describe what is expected at the failing position.
80    fn expected_char(self, expected: char) -> ContextParser<Self> {
81        ContextParser {
82            parser: self,
83            contexts: vec![StringContext::ExpectedChar(expected)],
84        }
85    }
86
87    /// Helper function to add multiple chars like [`Self::expected_char`].
88    fn expected_chars(self, expected: impl IntoIterator<Item = char>) -> ContextParser<Self> {
89        ContextParser {
90            parser: self,
91            contexts: expected
92                .into_iter()
93                .map(StringContext::ExpectedChar)
94                .collect(),
95        }
96    }
97
98    /// Add a string literal that is expected at the failing position.
99    fn expected_string(self, expected: &'static str) -> ContextParser<Self> {
100        ContextParser {
101            parser: self,
102            contexts: vec![StringContext::ExpectedString(expected)],
103        }
104    }
105
106    /// Helper function to add multiple strings literals like [`Self::expected_string`].
107    fn expected_strings<I>(self, expected: I) -> ContextParser<Self>
108    where
109        I: IntoIterator,
110        // NOTE: This is a **bit** of a hack.
111        // strum's `VALUES` slices are of type `&'static [&'static str]`.
112        // Calling a normal `.iter()` on it will result in the Item type being
113        // `&'static &'static str`.
114        //
115        // However, with generics, the type must match **exactly**, so `I::Item: &'static str`
116        // won't work. `Borrow` now saves the day by allowing deref coercion via the Borrow trait.
117        // That way, we can pass `MyEnum::VALUES` directly instead of having to do a
118        // `MyEnum::VALUES.iter().copied()` dance every time around.
119        I::Item: Borrow<&'static str>,
120    {
121        ContextParser {
122            parser: self,
123            // Read the `NOTE` above on why we need this.
124            #[expect(clippy::explicit_auto_deref)]
125            contexts: expected
126                .into_iter()
127                .map(|c| StringContext::ExpectedString(*c.borrow()))
128                .collect(),
129        }
130    }
131
132    /// Add a free-form text that describes what is expected at the failing position.
133    fn expected_text(self, expected: impl Into<String>) -> ContextParser<Self> {
134        ContextParser {
135            parser: self,
136            contexts: vec![StringContext::ExpectedText(expected.into())],
137        }
138    }
139
140    /// Add a free-form description of what went wrong at the failing position.
141    ///
142    /// Multiple description entries are joined without any spacing or characters.
143    fn description(self, description: impl Into<String>) -> ContextParser<Self> {
144        ContextParser {
145            parser: self,
146            contexts: vec![StringContext::Description(description.into())],
147        }
148    }
149}
150
151impl<'i, O, P> ContextExt<'i, O> for P where P: Parser<Input<'i>, O, ErrMode<ParseStack<'i>>> {}