Skip to main content

error_collection/
lib.rs

1#![doc = include_str!("../README.md")]
2
3use std::error::Error as StdError;
4use std::fmt;
5
6use derive_more::{Deref, DerefMut};
7
8/// An explicit collection of Errors.
9///
10/// In order to be more concise, consider simply using [Errors].
11pub type ErrorCollection = Errors;
12
13/// A collection of multiple `anyhow::Error`s.
14///
15/// This is helpful because we often don't want to bail at the first error.
16/// For example, take a simple method:
17///
18/// ```
19/// # use anyhow::{bail, Result};
20/// # #[derive(Debug, Clone, Copy)]
21/// # struct Header { hash: u64 }
22/// #
23/// # fn read_header(raw: &[u8]) -> Result<Header> {
24/// #     Ok(Header { hash: 0 })
25/// # }
26/// #
27/// # fn hasher(contents: &str) -> u64 {
28/// #     0
29/// # }
30/// #
31/// fn check_file_integrity(raw: Vec<u8>) -> anyhow::Result<()> {
32///   if raw.len() < 123 {
33///      bail!("Data too short")
34///   }
35///
36///   let Header { hash: expected_hash } = read_header(&raw[0..123])?;
37///   let contents = str::from_utf8(&raw[123..])?;
38///
39///   if contents.len() < 2 {
40///      bail!("Contents too short")
41///   }
42///
43///   let data_hash = hasher(&contents);
44///   if expected_hash != data_hash {
45///      bail!("Header hash mismatch: {expected_hash} {data_hash}")
46///   }
47///
48///   Ok(())
49/// }
50/// ```
51///
52/// We want the error to describe how the file is corrupted, but in our code we return
53/// at the very first instance of an error. We are loosing valuable information during
54/// runtime that could help debug a problem!
55///
56/// An [Errors] collection can help with this problem:
57///
58/// ```
59/// # use anyhow::{anyhow, bail, Result};
60/// # use error_collection::Errors;
61/// # #[derive(Debug, Clone, Copy)]
62/// # struct Header { hash: u64 }
63/// #
64/// # fn read_header(raw: &[u8]) -> Result<Header> {
65/// #     Ok(Header { hash: 0 })
66/// # }
67/// #
68/// # fn hasher(contents: &str) -> u64 {
69/// #     0
70/// # }
71/// #
72/// fn check_file_integrity(raw: Vec<u8>) -> anyhow::Result<()> {
73///   if raw.len() < 123 {
74///      bail!("Data too short")
75///   }
76///
77///   let mut errors = Errors::new();
78///
79///   // Convert the results to options
80///   let header = errors.collect(read_header(&raw[0..123]));
81///   let contents = errors.collect(str::from_utf8(&raw[123..]));
82///
83///   if let Some(contents) = contents && contents.len() < 2 {
84///     errors.append("Contents too short");
85///   }
86///
87///   if let Some(data_hash) = contents.map(hasher) &&
88///      let Some(Header { hash }) = header &&
89///      hash != data_hash {
90///      errors.push(anyhow!("Header hash mismatch: {hash} {data_hash}"));
91///   }
92///
93///   errors.as_result() // Ok(()) if there are no errors
94/// }
95/// ```
96///
97/// ```text
98/// 2 errors:
99///    1. Missing lightbulb
100///    2. Camera needs film
101/// ```
102#[derive(Default, Deref, DerefMut)]
103pub struct Errors(pub Vec<anyhow::Error>);
104
105impl Errors {
106    /// Creates an empty error collection.
107    pub fn new() -> Self {
108        Self::default()
109    }
110
111    /// Pushes an error to the back of this collection.
112    ///
113    /// Note: Pushing an Errors will nest that collection in this one.
114    /// See [Self::append] for an alternative that avoids nesting.
115    pub fn push(&mut self, err: impl Into<anyhow::Error>) {
116        self.0.push(err.into());
117    }
118
119    /// Appends another collection of [Errors] to the back of this one.
120    ///
121    /// Tip: You can append from `Options` and `Results` that implement `Into<anyhow::Error>`.
122    pub fn append(&mut self, err: impl Into<Self>) {
123        self.0.append(&mut err.into().0);
124    }
125
126    /// Unwraps the error contained in this Result. Errors get "collected" into the collection, and out comes the optional result.
127    pub fn collect<T, E>(&mut self, result: Result<T, E>) -> Option<T>
128    where
129        E: Into<anyhow::Error>,
130    {
131        match result {
132            Ok(value) => Some(value),
133            Err(err) => {
134                // Flatten out Errors
135                self.append(err.into());
136                None
137            }
138        }
139    }
140
141    /// Consumes the collection and returns the inner vector.
142    pub fn into_vec(self) -> Vec<anyhow::Error> {
143        self.0
144    }
145
146    /// Consumes the collection and returns a result.
147    pub fn as_result(mut self) -> anyhow::Result<()> {
148        match self.len() {
149            0 => Ok(()),
150            1 => Err(self.pop().unwrap()),
151            _ => Err(self.into()),
152        }
153    }
154}
155
156const PADDING: usize = 3;
157
158impl fmt::Debug for Errors {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        if f.alternate() {
161            write!(f, "Errors ")?;
162            let mut list = f.debug_list();
163            for error in self.iter() {
164                list.entry(error);
165            }
166            list.finish()
167        } else {
168            debug_collection(f, self, 0)
169        }
170    }
171}
172
173/// Custom debug formatter for Errors
174fn debug_collection(f: &mut fmt::Formatter<'_>, errors: &Errors, indent: usize) -> fmt::Result {
175    if errors.is_empty() {
176        writeln!(f, "none")
177    } else if errors.len() == 1 {
178        debug_error(f, &errors[0], indent)
179    } else {
180        writeln!(f, "{} errors:", errors.len())?;
181        for (idx, error) in errors.iter().enumerate() {
182            write!(f, "{}{}. ", spaces(indent + PADDING), idx + 1)?;
183            match error.downcast_ref::<Errors>() {
184                None => debug_error(f, error, indent + PADDING)?,
185                Some(errors) => debug_collection(f, errors, indent + PADDING)?,
186            }
187        }
188        Ok(())
189    }
190}
191
192/// Custom debug formatter for an anyhow::Error nested in a collection
193fn debug_error(f: &mut fmt::Formatter<'_>, error: &anyhow::Error, indent: usize) -> fmt::Result {
194    let padding = spaces(indent + PADDING);
195    let error_string = format!("{error:?}");
196    for (idx, line) in error_string.split('\n').enumerate() {
197        let padding = if idx == 0 { "" } else { padding };
198        writeln!(f, "{padding}{line}")?;
199    }
200    Ok(())
201}
202
203impl fmt::Display for Errors {
204    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
205        write!(f, "Errors: ")?;
206
207        if self.is_empty() {
208            return writeln!(f, "none");
209        }
210
211        let mut first = true;
212        display_collection(f, self, &mut first)?;
213        writeln!(f)
214    }
215}
216
217/// Custom display formatter for Errors
218fn display_collection(
219    f: &mut fmt::Formatter<'_>,
220    errors: &Errors,
221    first: &mut bool,
222) -> fmt::Result {
223    for error in errors.iter() {
224        match error.downcast_ref::<Errors>() {
225            Some(errors) => display_collection(f, errors, first)?,
226            None => display_error(f, error, first)?,
227        }
228    }
229    Ok(())
230}
231
232/// Custom display formatter for an anyhow::Error nested in a collection
233fn display_error(
234    f: &mut fmt::Formatter<'_>,
235    error: &anyhow::Error,
236    first: &mut bool,
237) -> fmt::Result {
238    if *first {
239        *first = false;
240    } else {
241        write!(f, ", ")?;
242    }
243
244    if f.alternate() {
245        write!(f, "{error:#}")
246    } else {
247        write!(f, "{error}")
248    }
249}
250
251/// Zero-alloc version of " ".repeat(x)
252fn spaces(indent: usize) -> &'static str {
253    &"                                "[..indent.min(32)]
254}
255
256impl StdError for Errors {}
257
258impl From<&str> for Errors {
259    fn from(value: &str) -> Self {
260        Self(vec![anyhow::anyhow!("{value}")])
261    }
262}
263
264impl From<String> for Errors {
265    fn from(value: String) -> Self {
266        Self(vec![anyhow::anyhow!(value)])
267    }
268}
269
270impl<T> From<Option<T>> for Errors
271where
272    T: Into<anyhow::Error>,
273{
274    fn from(result: Option<T>) -> Self {
275        match result {
276            Some(err) => err.into().into(),
277            None => Self::default(),
278        }
279    }
280}
281
282impl<T, E> From<Result<T, E>> for Errors
283where
284    E: Into<anyhow::Error>,
285{
286    fn from(result: Result<T, E>) -> Self {
287        match result {
288            Ok(_) => Self::default(),
289            Err(err) => err.into().into(),
290        }
291    }
292}
293
294impl From<Vec<anyhow::Error>> for Errors {
295    fn from(errors: Vec<anyhow::Error>) -> Self {
296        Self(errors)
297    }
298}
299
300impl From<anyhow::Error> for Errors {
301    fn from(error: anyhow::Error) -> Self {
302        match error.downcast::<Self>() {
303            Ok(errors) => errors,
304            Err(error) => Self(vec![error]),
305        }
306    }
307}
308
309impl<T> From<Errors> for anyhow::Result<T>
310where
311    T: Default,
312{
313    fn from(mut value: Errors) -> Self {
314        match value.len() {
315            0 => Ok(T::default()),
316            1 => Err(value.pop().unwrap()),
317            _ => Err(value.into()),
318        }
319    }
320}
321
322#[cfg(test)]
323mod tests {
324    use std::io;
325
326    use anyhow::{Context, anyhow};
327
328    use super::*;
329
330    #[test]
331    fn push() {
332        let mut nested = Errors::new();
333        nested.push(anyhow!("Generic error 1"));
334        nested.push(anyhow!("Generic error 2"));
335        nested.push(anyhow!("Generic error 3"));
336
337        let mut errors = Errors::new();
338        errors.push(nested);
339        errors.push(anyhow!("Generic error 4"));
340        errors.push(io::Error::from_raw_os_error(22));
341
342        assert_eq!(errors.len(), 3);
343    }
344
345    #[test]
346    fn append() {
347        let mut nested = Errors::new();
348        nested.append(vec![anyhow!("Generic error 1"), anyhow!("Generic error 2")]);
349
350        let mut errors = Errors::new();
351        errors.append(nested);
352        errors.append(anyhow!("Generic error 3"));
353
354        assert_eq!(errors.len(), 3);
355    }
356
357    #[test]
358    fn collect() {
359        let mut errors = Errors::new();
360
361        let result: Result<(), anyhow::Error> = Ok(());
362        assert_eq!(errors.collect(result), Some(()));
363
364        let result: Result<(), anyhow::Error> = Err(anyhow!("Generic error 1"));
365        assert_eq!(errors.collect(result), None);
366
367        assert_eq!(errors.len(), 1);
368    }
369
370    #[test]
371    fn collect_nested() {
372        let mut nested = Errors::new();
373        nested.push(anyhow!("Generic error 1"));
374        nested.push(anyhow!("Generic error 2"));
375        nested.push(anyhow!("Generic error 3"));
376
377        let mut errors = Errors::new();
378
379        let result: Result<(), Errors> = Err(nested);
380        assert_eq!(errors.collect(result), None);
381
382        assert_eq!(errors.len(), 3);
383    }
384
385    fn deeply_nested() -> Errors {
386        let mut child = Errors::new();
387        child.push(anyhow!("Generic error 2"));
388        child.push(anyhow!("Generic error 3\nnew line"));
389        child.push(Errors(vec![anyhow!("Generic error 4")]));
390
391        let mut parent = Errors::new();
392        parent.push(child);
393        parent.push(io::Error::from_raw_os_error(1));
394
395        let mut errors = Errors::new();
396        errors.push(
397            anyhow::Result::<()>::Err(anyhow!("Original error"))
398                .context("Generic error 1")
399                .unwrap_err(),
400        );
401        errors.push(parent);
402        errors
403    }
404
405    #[test]
406    fn display() {
407        let errors = deeply_nested();
408        assert_eq!(
409            format!("{errors}"),
410            "Errors: Generic error 1, Generic error 2, Generic error 3\n\
411            new line, Generic error 4, Operation not permitted (os error 1)\n"
412        );
413    }
414
415    #[test]
416    fn display_alternate() {
417        let errors = deeply_nested();
418        assert_eq!(
419            format!("{errors:#}"),
420            "Errors: Generic error 1: Original error, Generic error 2, Generic error 3\n\
421            new line, Generic error 4, Operation not permitted (os error 1)\n"
422        );
423    }
424
425    #[test]
426    fn debug() {
427        let errors = deeply_nested();
428        assert_eq!(
429            format!("{errors:?}"),
430            "2 errors:
431                1. Generic error 1
432                   \n      \
433                   Caused by:
434                       Original error
435                2. 2 errors:
436                   1. 3 errors:
437                      1. Generic error 2
438                      2. Generic error 3
439                         new line
440                      3. Generic error 4
441                   2. Operation not permitted (os error 1)\n"
442                .replace("\n             ", "\n")
443        );
444    }
445
446    #[test]
447    fn debug_alternate() {
448        let errors = deeply_nested();
449        println!("{errors:#?}");
450        assert_eq!(
451            format!("{errors:#?}"),
452            "Errors [
453                Error {
454                    context: \"Generic error 1\",
455                    source: \"Original error\",
456                },
457                Errors [
458                    Errors [
459                        \"Generic error 2\",
460                        \"Generic error 3\\nnew line\",
461                        Errors [
462                            \"Generic error 4\",
463                        ],
464                    ],
465                    Os {
466                        code: 1,
467                        kind: PermissionDenied,
468                        message: \"Operation not permitted\",
469                    },
470                ],
471            ]"
472            .replace("\n            ", "\n")
473        );
474    }
475}