error_accumulator/
lib.rs

1//! [`ErrorAccumulator`] is a utility to write parsing functions that work
2//! through as much data as possible collecting errors on the way instead of
3//! doing early returns on the first error.
4//!
5//! # General approach
6//!
7//! The idea is that an input is walked through all its fields, structs, and
8//! arrays. Along the way `ErrorAccumulator` is used to keep track of the
9//! input's parsing results so in case all parsing was successful the validated
10//! data can be returned and in case of at least one error all
11//! [`AccumulatedError`]s and their source can be named.
12
13#![deny(missing_debug_implementations)]
14#![deny(missing_docs)]
15
16use std::{error::Error, marker::PhantomData};
17
18use crate::{
19    builder::{ArrayBuilder, ErrorBuilderParent, FieldBuilder, StructBuilder},
20    cons::{Append, AsRefTuple, Nil, ToTuple},
21    construct::{Constructor, ListValidator},
22    error::AccumulatedError,
23    path::{FieldName, PathSegment, SourcePath},
24};
25
26pub mod builder;
27mod cons;
28pub mod construct;
29pub mod error;
30pub mod path;
31
32/// The entry-point to accumulate parsing results.
33///
34/// All parsing results are tracked, i.e. `Ok` values and errors alike.
35///
36/// Use the methods like [`field()`](Self::field), [`strukt()`](Self::strukt),
37/// or [`array()`](Self::array) to tell the accumulator from where in the input
38/// the next parsing results are derived.
39///
40/// The final [`analyse()`](Self::analyse) call returns all errors if at least
41/// one error was recorded else a tuple of all recorded `Ok` values is returned.
42/// There is also [`on_ok()`](Self::on_ok) to convert the recorded `Ok` values
43/// before retruning the tuple.
44#[derive(Debug)]
45pub struct ErrorAccumulator<List> {
46    errors: AccumulatedError,
47    values: List,
48    base: SourcePath,
49}
50
51/// Intermediate state when [`ErrorAccumulator::on_ok()`] was called.
52///
53/// See the method's documentation for more details.
54#[derive(Debug)]
55pub struct ErrorAccumulatorFinisher<List, Constructor, Out> {
56    accumulated_errors: AccumulatedError,
57    values: List,
58    constructor: Constructor,
59    _marker: PhantomData<Out>,
60}
61
62impl ErrorAccumulator<Nil> {
63    /// Create a new, empty `ErrorAccumulator`.
64    pub fn new() -> Self {
65        Self {
66            errors: Default::default(),
67            values: Nil,
68            base: Default::default(),
69        }
70    }
71}
72
73impl Default for ErrorAccumulator<Nil> {
74    fn default() -> Self {
75        Self::new()
76    }
77}
78
79impl<ChildValue, List> ErrorBuilderParent<ChildValue> for ErrorAccumulator<List>
80where
81    List: Append<ChildValue>,
82{
83    type AfterRecord = ErrorAccumulator<List::Output>;
84
85    fn finish_child_builder(
86        self,
87        child_result: Result<ChildValue, AccumulatedError>,
88    ) -> Self::AfterRecord {
89        let Self {
90            errors: mut accumulated_errors,
91            values,
92            base,
93        } = self;
94
95        let values = match child_result {
96            Ok(value) => values.append(value),
97            Err(errors) => {
98                accumulated_errors.merge(errors);
99                values.append(None)
100            }
101        };
102
103        ErrorAccumulator {
104            errors: accumulated_errors,
105            values,
106            base,
107        }
108    }
109}
110
111impl<List> ErrorAccumulator<List> {
112    /// Record a result of parsing a field of the input.
113    pub fn field<FieldValue, E>(
114        self,
115        field: FieldName,
116        result: Result<FieldValue, E>,
117    ) -> ErrorAccumulator<List::Output>
118    where
119        List: Append<FieldValue>,
120        E: Error + Send + Sync + 'static,
121    {
122        let path = self.base.join(PathSegment::Field(field));
123        FieldBuilder::new(self, path).value(result).finish()
124    }
125
126    /// Start a [`FieldBuilder`] to record results for parsing of a single input
127    /// field.
128    ///
129    /// This allows for more finegrained validation of a single input value,
130    /// e.g. like testing for different properties.
131    ///
132    /// See [`FieldBuilder`] for more information.
133    pub fn field_builder<FieldValue>(self, field: FieldName) -> FieldBuilder<Self, FieldValue, Nil>
134    where
135        List: Append<FieldValue>,
136    {
137        let path = self.base.join(PathSegment::Field(field));
138        FieldBuilder::new(self, path)
139    }
140
141    /// Start a [`StructBuilder`] to analyse the parsing results of a nested
142    /// struct of the input.
143    ///
144    /// This is mainly to record the correct source paths when walking the
145    /// input's structure.
146    ///
147    /// See [`StructBuilder`] for more information.
148    pub fn strukt<StructValue>(self, field: FieldName) -> StructBuilder<Self, StructValue, Nil>
149    where
150        List: Append<StructValue>,
151    {
152        let path = self.base.join(PathSegment::Field(field));
153        StructBuilder::new(self, path)
154    }
155
156    /// Start an [`ArrayBuilder`] to analyse the elements of a nested array of
157    /// the input.
158    ///
159    /// See [`ArrayBuilder`] for more information.
160    pub fn array<ElementValue>(self, field: FieldName) -> ArrayBuilder<Self, ElementValue>
161    where
162        List: Append<Vec<ElementValue>>,
163    {
164        let base = self.base.clone();
165        ArrayBuilder::new(self, base, field)
166    }
167
168    /// Run another validation step on the previously recorded `Ok` values if
169    /// there were no errors yet.
170    ///
171    /// In case an error was already recorded the `validator` is not executed.
172    ///
173    /// For an example, see the docs of [`StructBuilder::with_previous()`].
174    pub fn with_previous<Valid, T, E>(self, validator: Valid) -> ErrorAccumulator<List::Output>
175    where
176        Valid: ListValidator<List, T, E>,
177        List: AsRefTuple + Append<T>,
178        E: Error + Send + Sync + 'static,
179    {
180        let Self {
181            mut errors,
182            values,
183            base,
184        } = self;
185
186        let values = if errors.is_empty() {
187            let result = validator.validate(&values);
188            append_or_record(values, &base, result, &mut errors)
189        } else {
190            values.append(None)
191        };
192
193        ErrorAccumulator {
194            errors,
195            values,
196            base,
197        }
198    }
199
200    /// Provide a [`Constructor`] function that is called on
201    /// [`analyse()`](ErrorAccumulatorFinisher::analyse) in case all recorded
202    /// results (including nested results) where [`Ok`].
203    ///
204    /// - The input to the constructor are the recorded `Ok` values in order of
205    ///   recording.
206    /// - After providing the constructor no more results can be recorded.
207    pub fn on_ok<C, Out>(self, constructor: C) -> ErrorAccumulatorFinisher<List, C, Out>
208    where
209        List: ToTuple,
210        C: Constructor<List::List, Out>,
211    {
212        ErrorAccumulatorFinisher {
213            accumulated_errors: self.errors,
214            values: self.values,
215            constructor,
216            _marker: PhantomData,
217        }
218    }
219
220    /// Analyse all recorded results.
221    ///
222    /// If at least one error was recorded the [`AccumulatedError`]s are
223    /// returned else a tuple of all recorded `Ok` values in recording order is
224    /// returned.
225    pub fn analyse(self) -> Result<List::List, AccumulatedError>
226    where
227        List: ToTuple,
228    {
229        if self.errors.is_empty() {
230            // Would only panic if there were any errors.
231            Ok(self.values.unwrap_tuple())
232        } else {
233            Err(self.errors)
234        }
235    }
236}
237
238impl<List, Constr, Out> ErrorAccumulatorFinisher<List, Constr, Out>
239where
240    List: ToTuple,
241    Constr: Constructor<List::List, Out>,
242{
243    /// Like [`ErrorAccumulator::analyse()`] but the recorded `Ok` values are
244    /// processed by the provided [`Constructor`].
245    pub fn analyse(self) -> Result<Out, AccumulatedError> {
246        if self.accumulated_errors.is_empty() {
247            // Would only panic if there were any errors.
248            Ok(self.constructor.construct(self.values.unwrap_tuple()))
249        } else {
250            Err(self.accumulated_errors)
251        }
252    }
253}
254
255fn append_or_record<L, T, E>(
256    list: L,
257    path: &SourcePath,
258    result: Result<T, E>,
259    errors: &mut AccumulatedError,
260) -> L::Output
261where
262    L: Append<T>,
263    E: Error + Send + Sync + 'static,
264{
265    match result {
266        Ok(value) => list.append(value),
267        Err(error) => {
268            errors.append(path.clone(), error);
269            list.append(None)
270        }
271    }
272}
273
274#[cfg(test)]
275pub(crate) mod test_util {
276    use super::*;
277
278    pub fn n(name: &str) -> FieldName {
279        name.parse().unwrap()
280    }
281}
282
283#[cfg(test)]
284mod tests {
285    use std::num::{NonZeroI16, ParseIntError, TryFromIntError};
286
287    use super::*;
288    use crate::test_util::n;
289
290    #[derive(Debug, PartialEq, Eq)]
291    struct TestThing {
292        num: u32,
293        non_zero: NonZeroI16,
294    }
295
296    impl TestThing {
297        fn new(num: u32, non_zero: NonZeroI16) -> Self {
298            Self { num, non_zero }
299        }
300    }
301
302    #[test]
303    fn should_return_ok_values() {
304        let (num, non_zero) = ErrorAccumulator::new()
305            .field_builder(n("foo"))
306            .value("42".parse::<u32>())
307            .finish()
308            .field_builder(n("bar"))
309            .value(NonZeroI16::try_from(-5))
310            .finish()
311            .analyse()
312            .unwrap();
313
314        assert_eq!(num, 42);
315        assert_eq!(non_zero.get(), -5);
316    }
317
318    #[test]
319    fn should_return_on_one_error() {
320        let err = ErrorAccumulator::new()
321            .field("bar".parse().unwrap(), "42".parse::<u32>())
322            .field("foo".parse().unwrap(), NonZeroI16::try_from(0))
323            .analyse()
324            .unwrap_err();
325
326        assert_eq!(err.len(), 1);
327    }
328
329    #[test]
330    fn should_return_multiple_errors() {
331        let err = ErrorAccumulator::new()
332            .field(n("foo"), "foo".parse::<u32>())
333            .field(n("bar"), NonZeroI16::try_from(0))
334            .analyse()
335            .unwrap_err();
336
337        assert_eq!(err.get_by_type::<ParseIntError>().count(), 1);
338        assert_eq!(err.get_by_type::<TryFromIntError>().count(), 1);
339    }
340
341    #[test]
342    fn should_allow_construction_on_success() {
343        let thing = ErrorAccumulator::new()
344            .field(n("bar"), "42".parse::<u32>())
345            .field(n("foo"), NonZeroI16::try_from(-5))
346            .on_ok(TestThing::new)
347            .analyse()
348            .unwrap();
349
350        assert_eq!(thing, TestThing::new(42, NonZeroI16::new(-5).unwrap()))
351    }
352}