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>>> {}