process_results/
lib.rs

1//! This crate facilitates processing an iterator of some `Result` type.
2//! It provides the same functionality provided by
3//! [`Itertools::process_results`](https://docs.rs/itertools/0.10.1/itertools/fn.process_results.html),
4//! hence the name, but with a more much ergonomic interface, some extra
5//! helper methods and a macro to reduce boiler-plate.   
6//!
7//! At a high level this crate is composed of 3 items: an extension trait [`IterResult`] that is
8//! implemented for all [iterators][Iterator] of [`Result`] type, [`Fallible`] struct that
9//! wraps the iterator, and [`ErrorCollector`][ErrorCollector] that stores the errors.
10//!
11//! [`IterResult`] is an extension trait that contains methods that consumes itself and wrap it
12//! with [`Fallible`] and appropriate the error collector.
13//!
14//! [`Fallible`] has methods [`Fallible::process`] and [`Fallible::process_no_discard`]
15//! that accept a closure, which allows the caller to process an `impl Iterator<Item = Result<T, E>>`
16//! as an `impl Iterator<Item = T>` and to handle the errors in a composable manner.
17//!
18//! [`ErrorCollector`] is a trait that let the implementor determine how errors are stored, whether
19//! or not an error shall stop the iteration, as well as how should errors be returned.  
20//! Implementations are provided for common types like
21//! [`Option`][ErrorCollector#impl-ErrorCollector-for-Option<E>]
22//! and [`Vec`][ErrorCollector#impl-ErrorCollector-for-Vec<E>] to allow the iteration to stop and
23//! return the first error encountered and return, or to finish the iteration and stop all errors
24//! in a [`Vec`]. Unit struct [`Ignore`] is also provided that ignores all the errors encountered.
25//!
26//! # Examples
27//!
28//! ### Simple Iteration
29//! ```
30//! use process_results::IterResult;
31//!
32//! let v = vec![Ok(1i64), Ok(4), Ok(-3), Err("Error"), Ok(10)];
33//! let res: Result<i64, _> = v.into_iter().failfast().process(|it| it.sum());
34//! assert_eq!(res, Err("Error"));
35//! ```
36//!
37//! ### Accumulate Errors
38//! ```
39//! use process_results::IterResult;
40//!
41//! let v = vec![
42//!     Ok(1i64),
43//!     Err("Error1"),
44//!     Ok(4),
45//!     Ok(-3),
46//!     Err("Error2"),
47//!     Ok(10),
48//! ];
49//! let res: Result<i64, _> = v
50//!     .into_iter()
51//!     .accumulate()
52//!     .process(|it| it.sum());
53//! assert_eq!(res, Err(vec!["Error1", "Error2"]));
54//! ```
55//!
56//! ### Nested Errors
57//! Here is an example that read lines from files in a folder, parse each line as `i32`
58//! while saving the lines that cannot be parsed successfully.
59//!
60//! ```
61//! use process_results::*;
62//! use process_results::fallible;
63//! use std::path::Path;
64//! use std::fs::File;
65//! use std::io::BufReader;
66//! use std::io::BufRead;
67//!
68//! let res_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("res");
69//! let res_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("res");
70//! let (sum, err) = res_dir
71//!     .read_dir()
72//!     .unwrap()
73//!     .failfast()
74//!     .process(|it| {
75//!         it.filter(|entry| entry.file_type().map_or(false, |t| t.is_file()))
76//!             .map(|entry| {
77//!                 File::open(entry.path())
78//!                     .map(BufReader::new)
79//!                     .map(|f| (entry.file_name(), f))
80//!             })
81//!             .failfast()
82//!             .process(|it| {
83//!                 it.flat_map(|(name, f)| {
84//!                     f.lines()
85//!                         .enumerate()
86//!                         .map(move |(ln_no, ln)| ln.map(|ln| (name.clone(), ln_no, ln)))
87//!                 })
88//!                 .failfast()
89//!                 .process(|it| {
90//!                     it.map(|(name, ln_no, ln)| {
91//!                         ln.parse::<i32>().map_err(|_e| {
92//!                             format!("{}-{}: {}", name.to_string_lossy(), ln_no + 1, ln)
93//!                         })
94//!                     })
95//!                     .accumulate()
96//!                     .process_no_discard::<_, i32>(|it| it.sum())
97//!                 })
98//!             })
99//!     })
100//!     .unwrap()
101//!     .unwrap()
102//!     .unwrap();
103//! assert_eq!(sum, 11966);
104//! assert_eq!(
105//!     err.unwrap(),
106//!     vec![
107//!         "test1.txt-7: sadfs",
108//!         "test2.txt-3: 1000000000000000000000000000000000000000000000000000000000",
109//!         "test2.txt-6: hello world",
110//!         "test2.txt-8: 1.35"
111//!     ]
112//! );
113//! ```
114//!
115//!
116//! ### Nested Errors with Macro
117//! The same code as the last one, but utilizing macro [`fallible!`].
118//!
119//! ```
120//! use process_results::*;
121//! use process_results::fallible;
122//! use std::path::Path;
123//! use std::fs::File;
124//! use std::io::BufReader;
125//! use std::io::BufRead;
126//!
127//! let res_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("res");
128//! let (sum, err) = fallible!(
129//!     res_dir.read_dir().unwrap().failfast(),
130//!     |it| it
131//!         .filter(|entry| entry.file_type().map_or(false, |t| t.is_file()))
132//!         .map(|entry| File::open(entry.path()).map(BufReader::new)
133//!         .map(|f| (entry.file_name(), f))).failfast(),
134//!     |it| it.flat_map(
135//!         |(name, f)| f.lines()
136//!             .enumerate()
137//!             .map(move |(ln_no, ln)| ln.map(|ln| (name.clone(), ln_no, ln)))
138//!     ).failfast(),
139//!     |it| it
140//!         .map(
141//!             |(name, ln_no, ln)| ln.parse::<i32>()
142//!                 .map_err(|_e| format!("{}-{}: {}", name.to_string_lossy(), ln_no + 1, ln))
143//!         )
144//!         .accumulate(),
145//!     no_discard i32: |it| it.sum()
146//! ).unwrap().unwrap().unwrap();
147//! assert_eq!(sum, 11966);
148//! assert_eq!(
149//!     err.unwrap(),
150//!     vec![
151//!         "test1.txt-7: sadfs",
152//!         "test2.txt-3: 1000000000000000000000000000000000000000000000000000000000",
153//!         "test2.txt-6: hello world",
154//!         "test2.txt-8: 1.35"
155//!     ]
156//! );
157//! ```
158
159use crate::errors::Ignore;
160use crate::raw_iter::RawIter;
161use errors::ErrorCollector;
162
163pub mod errors;
164pub mod raw_iter;
165
166/// An extension trait implemented for all iterators of `Result` types.  
167pub trait IterResult: Iterator<Item = Result<Self::Ok, Self::Error>> + Sized {
168    /// The type wrapped by the `Ok` variant of the `Result` type
169    type Ok;
170    /// The type wrapped by the `Err` variant of the `Result` type
171    type Error;
172
173    /// Produces a version of [`Fallible`] that stops iterating upon encountering the 1st error.
174    #[inline]
175    fn failfast(self) -> Fallible<Self, Option<Self::Error>> {
176        self.fallible()
177    }
178
179    /// Produces a version of [`Fallible`] that keeps iterating and ignores all errors.
180    #[inline]
181    fn ignore(self) -> Fallible<Self, Ignore> {
182        self.fallible()
183    }
184
185    /// Produces a version of [`Fallible`] that keeps iterating and stores all errors in a `Vec`.
186    #[inline]
187    fn accumulate(self) -> Fallible<Self, Vec<Self::Error>> {
188        self.fallible()
189    }
190
191    /// Produces a version of [`Fallible`] with a custom type of [`ErrorCollector`]
192    #[inline]
193    fn fallible<C: ErrorCollector<Self::Error>>(self) -> Fallible<Self, C> {
194        self.with_collector(C::empty())
195    }
196
197    /// Produces a version of [`Fallible`] with an existing value of [`ErrorCollector`]
198    #[inline]
199    fn with_collector<C: ErrorCollector<Self::Error>>(self, collector: C) -> Fallible<Self, C> {
200        Fallible {
201            iter: self,
202            errors: collector,
203        }
204    }
205}
206
207impl<I, T, E> IterResult for I
208where
209    I: Iterator<Item = Result<T, E>>,
210{
211    type Ok = T;
212    type Error = E;
213}
214
215#[derive(Debug, Clone)]
216pub struct Fallible<I, C> {
217    iter: I,
218    errors: C,
219}
220
221#[must_use = "iterator adaptors are lazy and do nothing unless consumed"]
222impl<I, C> Fallible<I, C>
223where
224    I: IterResult,
225    C: ErrorCollector<I::Error>,
226{
227    /// “Lift” a function of the values of an iterator so that it can process an iterator of [`Result`]
228    /// values instead.
229    // fixme: I really don't know how to word this in a better way.
230    ///
231    /// `f` is a closure that takes [`RawIter`], an iterator adapter that wraps the inner
232    /// iterator and implements `Iterator<Item=I::OK>`.
233    ///
234    /// Returns either the returned value of closure `f` wrapped in the `Ok` variant, or the error
235    /// wrapped in `Err` variant, in a way specified by `C`'s implementation of [`ErrorCollector`].
236    #[inline]
237    pub fn process<F, B>(self, f: F) -> Result<B, C::Collection>
238    where
239        F: FnOnce(RawIter<I, C>) -> B,
240    {
241        let Self { iter, mut errors } = self;
242        let raw_iter = RawIter {
243            iter,
244            errors: &mut errors,
245        };
246        let b = f(raw_iter);
247        errors.with_value(b)
248    }
249
250    /// “Lift” a function of the values of an iterator so that it can process an iterator of [`Result`]
251    /// values instead.
252    // fixme: I really don't know how to word this in a better way.
253    ///
254    /// `f` is a closure that takes [`RawIter`], an iterator adapter that wraps the inner
255    /// iterator and implements `Iterator<Item=I::OK>`.
256    ///
257    /// Returns both the returned value of closure `f` and None if the wrapped iterator runs to
258    /// completion; otherwise, returns the intermediate value produced so far and the errors in a
259    /// way specified by `C`'s implementation of [`ErrorCollector`]
260    #[inline]
261    pub fn process_no_discard<F, B>(self, f: F) -> (B, Option<C::Collection>)
262    where
263        F: FnOnce(RawIter<I, C>) -> B,
264    {
265        let Self { iter, mut errors } = self;
266        let raw_iter = RawIter {
267            iter,
268            errors: &mut errors,
269        };
270        let b = f(raw_iter);
271        (b, errors.with_value(()).err())
272    }
273}
274
275/// A macro used to reduce boilerplate when nesting multiple calls to [`process`][Fallible::process]
276/// or [`process_no_discard`][Fallible::process_no_discard] inside each other.
277///
278/// [`Fallible`] and [`IterResult`] must be imported to use the macro.
279#[macro_export]
280macro_rules! fallible {
281    ($base:expr) => {
282        $base
283    };
284    ($base:expr, $($b_type:ty :)? | $id:ident $(: $ty:ty)? | $expr:expr $(, $($tail:tt)+)? ) => {
285        Fallible::process$(::<_, $b_type>)?($base, |$id $(: $ty:ty)?| fallible!($expr $(, $($tail)+)?))
286    };
287    ($base:expr, $($b_type:ty :)? move | $id:ident $(: $ty:ty)? | $expr:expr $(, $($tail:tt)+)? ) => {
288        Fallible::process$(::<_, $b_type>)?($base, move |$id $(: $ty:ty)?| fallible!($expr $(, $($tail)+)?))
289    };
290    ($base:expr, no_discard $($b_type:ty :)? | $id:ident $(: $ty:ty)? | $expr:expr $(, $($tail:tt)+)? ) => {
291        Fallible::process_no_discard$(::<_, $b_type>)?($base, |$id $(: $ty:ty)?| fallible!($expr $(, $($tail)+)?))
292    };
293    ($base:expr, no_discard $($b_type:ty :)? move | $id:ident $(: $ty:ty)? | $expr:expr $(, $($tail:tt)+)? ) => {
294        Fallible::process_no_discard$(::<_, $b_type>)?($base, move |$id $(: $ty:ty)?| fallible!($expr $(, $($tail)+)?))
295    };
296}
297
298#[cfg(test)]
299mod tests {
300    use crate::Fallible;
301    use crate::IterResult;
302    use std::fs::File;
303    use std::io::{BufRead, BufReader};
304    use std::path::Path;
305
306    #[test]
307    fn test_failfast() {
308        let v = vec![Ok(1i64), Ok(4), Ok(-3), Err("Error"), Ok(10)];
309        let res: Result<i64, _> = v.into_iter().failfast().process(|it| it.sum());
310        assert_eq!(res, Err("Error"));
311    }
312
313    #[test]
314    fn test_success() {
315        let v: Vec<Result<_, &str>> = vec![Ok(1i64), Ok(4), Ok(-3), Ok(10)];
316        let res: i64 = v
317            .iter()
318            .map(Result::as_ref)
319            .failfast()
320            .process(|it| it.sum())
321            .unwrap();
322        assert_eq!(res, 12);
323    }
324
325    #[test]
326    fn test_filter() {
327        let v = vec![Ok(1i64), Ok(4), Ok(-3), Err("Error"), Ok(10)];
328        let res: i64 = v.into_iter().ignore().process(|it| it.sum()).unwrap();
329        assert_eq!(res, 12);
330    }
331
332    #[test]
333    fn test_accumulator() {
334        let v = vec![
335            Ok(1i64),
336            Err("Error1"),
337            Ok(4),
338            Ok(-3),
339            Err("Error2"),
340            Ok(10),
341        ];
342        let res: Result<i64, _> = v
343            .into_iter()
344            .with_collector(Vec::new())
345            .process(|it| it.sum());
346        assert_eq!(res, Err(vec!["Error1", "Error2"]));
347    }
348
349    #[test]
350    fn test_recursive() -> eyre::Result<()> {
351        let res_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("res");
352        let sum = res_dir.read_dir()?.failfast().process(|it| {
353            it.filter(|entry| entry.file_type().map_or(false, |t| t.is_file()))
354                .map(|entry| File::open(entry.path()))
355                .failfast()
356                .process(|it| {
357                    it.map(BufReader::new)
358                        .flat_map(|f| f.lines())
359                        .failfast()
360                        .process(|it| {
361                            it.map(|ln| ln.parse::<i32>())
362                                .ignore()
363                                .process::<_, i32>(|it| it.sum())
364                        })
365                })
366        })????;
367        assert_eq!(sum, 11966);
368        Ok(())
369    }
370
371    #[test]
372    fn test_macro() -> eyre::Result<()> {
373        let res_dir = Path::new(env!("CARGO_MANIFEST_DIR")).join("res");
374        let (sum, err) = fallible!(
375            res_dir.read_dir()?.failfast(),
376            |it| it
377                .filter(|entry| entry.file_type().map_or(false, |t| t.is_file()))
378                .map(|entry| File::open(entry.path()).map(BufReader::new)
379                .map(|f| (entry.file_name(), f))).failfast(),
380            |it| it.flat_map(
381                |(name, f)| f.lines()
382                    .enumerate()
383                    .map(move |(ln_no, ln)| ln.map(|ln| (name.clone(), ln_no, ln)))
384            ).failfast(),
385            |it| it
386                .map(
387                    |(name, ln_no, ln)| ln.parse::<i32>()
388                        .map_err(|_e| format!("{}-{}: {}", name.to_string_lossy(), ln_no + 1, ln))
389                )
390                .accumulate(),
391            no_discard i32: |it| it.sum()
392        )???;
393        assert_eq!(sum, 11966);
394        assert_eq!(
395            err.unwrap(),
396            vec![
397                "test1.txt-7: sadfs",
398                "test2.txt-3: 1000000000000000000000000000000000000000000000000000000000",
399                "test2.txt-6: hello world",
400                "test2.txt-8: 1.35"
401            ]
402        );
403        Ok(())
404    }
405}