error_stack/ext/
iter.rs

1#![expect(deprecated, reason = "We use `Context` to maintain compatibility")]
2
3use crate::{Context, Report};
4
5// inspired by the implementation in `std`, see: https://doc.rust-lang.org/1.81.0/src/core/iter/adapters/mod.rs.html#157
6// except with the removal of the Try trait, as it is unstable.
7struct ReportShunt<'a, I, T, C> {
8    iter: I,
9
10    report: &'a mut Option<Report<[C]>>,
11    context_len: usize,
12    context_bound: usize,
13
14    _marker: core::marker::PhantomData<fn() -> *const T>,
15}
16
17impl<I, T, R, C> Iterator for ReportShunt<'_, I, T, C>
18where
19    I: Iterator<Item = Result<T, R>>,
20    R: Into<Report<[C]>>,
21{
22    type Item = T;
23
24    fn next(&mut self) -> Option<Self::Item> {
25        loop {
26            if self.context_len >= self.context_bound {
27                return None;
28            }
29
30            let item = self.iter.next()?;
31            let item = item.map_err(Into::into);
32
33            match (item, self.report.as_mut()) {
34                (Ok(output), None) => return Some(output),
35                (Ok(_), Some(_)) => {
36                    // we're now just consuming the iterator to return all related errors
37                    // so we can just ignore the output
38                }
39                (Err(error), None) => {
40                    *self.report = Some(error);
41                    self.context_len += 1;
42                }
43                (Err(error), Some(report)) => {
44                    report.append(error);
45                    self.context_len += 1;
46                }
47            }
48        }
49    }
50
51    fn size_hint(&self) -> (usize, Option<usize>) {
52        if self.report.is_some() {
53            (0, Some(0))
54        } else {
55            let (_, upper) = self.iter.size_hint();
56
57            (0, upper)
58        }
59    }
60}
61
62fn try_process_reports<I, T, R, C, F, U>(
63    iter: I,
64    bound: Option<usize>,
65    mut collect: F,
66) -> Result<U, Report<[C]>>
67where
68    I: Iterator<Item = Result<T, R>>,
69    R: Into<Report<[C]>>,
70    for<'a> F: FnMut(ReportShunt<'a, I, T, C>) -> U,
71{
72    let mut report = None;
73    let shunt = ReportShunt {
74        iter,
75        report: &mut report,
76        context_len: 0,
77        context_bound: bound.unwrap_or(usize::MAX),
78        _marker: core::marker::PhantomData,
79    };
80
81    let value = collect(shunt);
82    report.map_or_else(|| Ok(value), |report| Err(report))
83}
84
85/// An extension trait for iterators that enables error-aware collection of items.
86///
87/// This trait enhances iterators yielding `Result` items by providing methods to
88/// collect successful items into a container while aggregating encountered errors.
89///
90/// # Performance Considerations
91///
92/// These methods may have performance implications as they potentially iterate
93/// through the entire collection, even after encountering errors.
94///
95/// # Unstable Feature
96///
97/// This trait is currently available only under the `unstable` feature flag and
98/// does not adhere to semver guarantees. Its API may change in future releases.
99///
100/// [`Report`]: crate::Report
101pub trait TryReportIteratorExt<C> {
102    /// The type of the successful items in the iterator.
103    type Ok;
104
105    /// Collects the successful items from the iterator into a container, or returns all errors that
106    /// occured.
107    ///
108    /// This method attempts to collect all successful items from the iterator into the specified
109    /// container type. If an error is encountered during iteration, the method will exhaust the
110    /// iterator and return a `Report` containing all errors encountered.
111    ///
112    /// # Errors
113    ///
114    /// If any error is encountered during iteration, the method will return a `Report` containing
115    /// all errors encountered up to that point.
116    ///
117    /// # Examples
118    ///
119    /// ```
120    /// use error_stack::{Report, TryReportIteratorExt};
121    /// use std::io;
122    ///
123    /// fn fetch_fail() -> Result<u8, Report<io::Error>> {
124    ///    # stringify! {
125    ///    ...
126    ///    # };
127    ///    # Err(Report::from(io::Error::new(io::ErrorKind::Other, "error")))
128    /// }
129    ///
130    /// let results = [Ok(1_u8), fetch_fail(), Ok(2), fetch_fail(), fetch_fail()];
131    /// let collected: Result<Vec<_>, _> = results.into_iter().try_collect_reports();
132    /// let error = collected.expect_err("multiple calls should have failed");
133    ///
134    /// assert_eq!(error.current_contexts().count(), 3);
135    /// ```
136    fn try_collect_reports<A>(self) -> Result<A, Report<[C]>>
137    where
138        A: FromIterator<Self::Ok>;
139
140    /// Collects the successful items from the iterator into a container or returns all errors up to
141    /// the specified bound.
142    ///
143    /// This method is similar to [`try_collect_reports`], but it limits the number of errors
144    /// collected to the specified `bound`. If the number of errors encountered exceeds the bound,
145    /// the method stops collecting errors and returns the collected errors up to that point.
146    ///
147    /// [`try_collect_reports`]: TryReportIteratorExt::try_collect_reports
148    ///
149    /// # Errors
150    ///
151    /// If any error is encountered during iteration, the method will return a `Report` containing
152    /// all errors encountered up to the specified bound.
153    ///
154    /// # Examples
155    ///
156    /// ```
157    /// use error_stack::{Report, TryReportIteratorExt};
158    /// use std::io;
159    ///
160    /// fn fetch_fail() -> Result<u8, Report<io::Error>> {
161    ///    # stringify! {
162    ///    ...
163    ///    # };
164    ///    # Err(Report::from(io::Error::new(io::ErrorKind::Other, "error")))
165    /// }
166    ///
167    /// let results = [Ok(1_u8), fetch_fail(), Ok(2), fetch_fail(), fetch_fail()];
168    /// let collected: Result<Vec<_>, _> = results.into_iter().try_collect_reports_bounded(2);
169    /// let error = collected.expect_err("should have failed");
170    ///
171    /// assert_eq!(error.current_contexts().count(), 2);
172    /// ```
173    fn try_collect_reports_bounded<A>(self, bound: usize) -> Result<A, Report<[C]>>
174    where
175        A: FromIterator<Self::Ok>;
176}
177
178impl<T, C, R, I> TryReportIteratorExt<C> for I
179where
180    I: Iterator<Item = Result<T, R>>,
181    R: Into<Report<[C]>>,
182    C: Context,
183{
184    type Ok = T;
185
186    fn try_collect_reports<A>(self) -> Result<A, Report<[C]>>
187    where
188        A: FromIterator<Self::Ok>,
189    {
190        try_process_reports(self, None, |shunt| shunt.collect())
191    }
192
193    fn try_collect_reports_bounded<A>(self, bound: usize) -> Result<A, Report<[C]>>
194    where
195        A: FromIterator<Self::Ok>,
196    {
197        try_process_reports(self, Some(bound), |shunt| shunt.collect())
198    }
199}
200#[cfg(test)]
201mod tests {
202    #![allow(clippy::integer_division_remainder_used)]
203    use alloc::{collections::BTreeSet, vec::Vec};
204    use core::fmt;
205
206    use super::*;
207
208    #[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
209    struct CustomError(usize);
210
211    impl fmt::Display for CustomError {
212        fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
213            write!(fmt, "CustomError({})", self.0)
214        }
215    }
216
217    impl core::error::Error for CustomError {}
218
219    #[test]
220    fn try_collect_multiple_errors() {
221        let iter = (0..5).map(|i| {
222            if i % 2 == 0 {
223                Ok(i)
224            } else {
225                Err(Report::new(CustomError(i)))
226            }
227        });
228
229        let result: Result<Vec<_>, Report<[CustomError]>> = iter.try_collect_reports();
230        let report = result.expect_err("should have failed");
231
232        let contexts: BTreeSet<_> = report.current_contexts().collect();
233        assert_eq!(contexts.len(), 2);
234        assert!(contexts.contains(&CustomError(1)));
235        assert!(contexts.contains(&CustomError(3)));
236    }
237
238    #[test]
239    fn try_collect_multiple_errors_bounded() {
240        let iter = (0..10).map(|i| {
241            if i % 2 == 0 {
242                Ok(i)
243            } else {
244                Err(Report::new(CustomError(i)))
245            }
246        });
247
248        let result: Result<Vec<_>, Report<[CustomError]>> = iter.try_collect_reports_bounded(3);
249        let report = result.expect_err("should have failed");
250
251        let contexts: BTreeSet<_> = report.current_contexts().collect();
252        assert_eq!(contexts.len(), 3);
253        assert!(contexts.contains(&CustomError(1)));
254        assert!(contexts.contains(&CustomError(3)));
255        assert!(contexts.contains(&CustomError(5)));
256    }
257
258    #[test]
259    fn try_collect_no_errors() {
260        let iter = (0..5).map(Result::<_, Report<CustomError>>::Ok);
261
262        let result: Result<Vec<_>, Report<[CustomError]>> = iter.try_collect_reports();
263        let values = result.expect("should have succeeded");
264
265        assert_eq!(values, [0, 1, 2, 3, 4]);
266    }
267
268    #[test]
269    fn try_collect_multiple_errors_expanded() {
270        let iter = (0..5).map(|i| {
271            if i % 2 == 0 {
272                Ok(i)
273            } else {
274                Err(Report::new(CustomError(i)).expand())
275            }
276        });
277
278        let result: Result<Vec<_>, Report<[CustomError]>> = iter.try_collect_reports();
279        let report = result.expect_err("should have failed");
280
281        let contexts: BTreeSet<_> = report.current_contexts().collect();
282        assert_eq!(contexts.len(), 2);
283        assert!(contexts.contains(&CustomError(1)));
284        assert!(contexts.contains(&CustomError(3)));
285    }
286}