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}