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///   let Header { hash: expected_hash } = read_header(&raw[0..123])?;
33///   let contents = str::from_utf8(&raw[123..])?;
34///
35///   if contents.len() < 2 {
36///      bail!("Contents too short")
37///   }
38///
39///   let data_hash = hasher(&contents);
40///   if expected_hash != data_hash {
41///      bail!("Header hash mismatch: {expected_hash} {data_hash}")
42///   }
43///
44///   Ok(())
45/// }
46/// ```
47///
48/// We want the error to describe how the file is corrupted, but in our code we return
49/// at the very first instance of an error. We are loosing valuable information during
50/// runtime that could help debug a problem!
51///
52/// An [Errors] collection can help with this problem:
53///
54/// ```
55/// # use anyhow::{anyhow, Result};
56/// # use error_collection::Errors;
57/// # #[derive(Debug, Clone, Copy)]
58/// # struct Header { hash: u64 }
59/// #
60/// # fn read_header(raw: &[u8]) -> Result<Header> {
61/// #     Ok(Header { hash: 0 })
62/// # }
63/// #
64/// # fn hasher(contents: &str) -> u64 {
65/// #     0
66/// # }
67/// #
68/// fn check_file_integrity(raw: Vec<u8>) -> anyhow::Result<()> {
69///   let mut errors = Errors::new();
70///
71///   // Convert the results to options
72///   let header = errors.collect(read_header(&raw[0..123]));
73///   let contents = errors.collect(str::from_utf8(&raw[123..]));
74///
75///   if let Some(contents) = contents && contents.len() < 2 {
76///     errors.append("Contents too short");
77///   }
78///
79///   if let Some(data_hash) = contents.map(hasher) &&
80///      let Some(Header { hash }) = header &&
81///      hash != data_hash {
82///      errors.push(anyhow!("Header hash mismatch: {hash} {data_hash}"));
83///   }
84///
85///   errors.as_result() // Ok(()) if there are no errors
86/// }
87/// ```
88///
89/// ```text
90/// 2 errors:
91///    1. Missing lightbulb
92///    2. Camera needs film
93/// ```
94#[derive(Debug, Default, Deref, DerefMut)]
95pub struct Errors(pub Vec<anyhow::Error>);
96
97impl Errors {
98    /// Creates an empty error collection.
99    pub fn new() -> Self {
100        Self::default()
101    }
102
103    /// Pushes an error to the back of this collection.
104    ///
105    /// Note: Pushing an Errors will nest that collection in this one.
106    /// See [Self::append] for an alternative that avoids nesting.
107    pub fn push(&mut self, err: impl Into<anyhow::Error>) {
108        self.0.push(err.into());
109    }
110
111    /// Appends another collection of [Errors] to the back of this one.
112    ///
113    /// Tip: You can append from `Options` and `Results` that implement `Into<anyhow::Error>`.
114    pub fn append(&mut self, err: impl Into<Self>) {
115        self.0.append(&mut err.into().0);
116    }
117
118    /// Unwraps the error contained in this Result. Errors get "collected" into the collection, and out comes the optional result.
119    pub fn collect<T, E>(&mut self, result: Result<T, E>) -> Option<T>
120    where
121        E: Into<anyhow::Error>,
122    {
123        match result.into() {
124            Ok(value) => Some(value),
125            Err(err) => {
126                // Flatten out Errors
127                self.append(err.into());
128                None
129            }
130        }
131    }
132
133    /// Consumes the collection and returns the inner vector.
134    pub fn into_vec(self) -> Vec<anyhow::Error> {
135        self.0
136    }
137
138    /// Consumes the collection and returns a result.
139    pub fn as_result(mut self) -> anyhow::Result<()> {
140        match self.len() {
141            0 => Ok(()),
142            1 => Err(self.pop().unwrap()),
143            _ => Err(self.into()),
144        }
145    }
146}
147
148const PADDING: usize = 3;
149
150impl fmt::Display for Errors {
151    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
152        format_errors(Ok(self), f, 0)
153    }
154}
155
156fn format_errors(
157    error: Result<&Errors, &anyhow::Error>,
158    f: &mut fmt::Formatter<'_>,
159    indent: usize,
160) -> fmt::Result {
161    match error {
162        Err(error) if f.alternate() => write_padded(&format!("{:#}", error), f, indent),
163        Err(error) => write_padded(&format!("{}", error), f, indent),
164        Ok(errors) if errors.len() == 0 => writeln!(f, "none"),
165        Ok(errors) if errors.len() == 1 => format_errors(Err(&errors[0]), f, indent),
166        Ok(errors) => {
167            writeln!(f, "{} errors:", errors.len())?;
168            for (idx, err) in errors.iter().enumerate() {
169                write!(f, "{}{}. ", " ".repeat(indent + PADDING), idx + 1)?;
170                let error = err.downcast_ref::<Errors>().ok_or(err);
171                format_errors(error, f, indent + PADDING)?;
172            }
173
174            Ok(())
175        }
176    }
177}
178
179fn spaces(padding: usize) -> &'static str {
180    &"                                        "[..padding]
181}
182
183fn write_padded(string: &str, f: &mut fmt::Formatter<'_>, padding: usize) -> fmt::Result {
184    let padding = spaces(padding + PADDING);
185    for (idx, line) in string.split('\n').enumerate() {
186        let padding = if idx == 0 { "" } else { &padding };
187        writeln!(f, "{padding}{line}")?;
188    }
189
190    Ok(())
191}
192
193impl StdError for Errors {}
194
195impl From<&str> for Errors {
196    fn from(value: &str) -> Self {
197        Self(vec![anyhow::anyhow!("{value}")])
198    }
199}
200
201impl From<String> for Errors {
202    fn from(value: String) -> Self {
203        Self(vec![anyhow::anyhow!(value)])
204    }
205}
206
207impl<T> From<Option<T>> for Errors
208where
209    T: Into<anyhow::Error>,
210{
211    fn from(result: Option<T>) -> Self {
212        match result {
213            Some(err) => err.into().into(),
214            None => Self::default(),
215        }
216    }
217}
218
219impl<T, E> From<Result<T, E>> for Errors
220where
221    E: Into<anyhow::Error>,
222{
223    fn from(result: Result<T, E>) -> Self {
224        match result {
225            Ok(_) => Self::default(),
226            Err(err) => err.into().into(),
227        }
228    }
229}
230
231impl From<Vec<anyhow::Error>> for Errors {
232    fn from(errors: Vec<anyhow::Error>) -> Self {
233        Self(errors)
234    }
235}
236
237impl From<anyhow::Error> for Errors {
238    fn from(error: anyhow::Error) -> Self {
239        match error.downcast::<Self>() {
240            Ok(errors) => errors,
241            Err(error) => Self(vec![error]),
242        }
243    }
244}
245
246impl<T> From<Errors> for anyhow::Result<T>
247where
248    T: Default,
249{
250    fn from(mut value: Errors) -> Self {
251        match value.len() {
252            0 => Ok(T::default()),
253            1 => Err(value.pop().unwrap()),
254            _ => Err(value.into()),
255        }
256    }
257}
258
259#[cfg(test)]
260mod tests {
261    use std::io;
262
263    use anyhow::anyhow;
264
265    use super::*;
266
267    #[test]
268    fn push() {
269        let mut nested = Errors::new();
270        nested.push(anyhow!("Generic error 1"));
271        nested.push(anyhow!("Generic error 2"));
272        nested.push(anyhow!("Generic error 3"));
273
274        let mut errors = Errors::new();
275        errors.push(nested);
276        errors.push(anyhow!("Generic error 4"));
277        errors.push(io::Error::from_raw_os_error(22));
278
279        assert_eq!(errors.len(), 3);
280    }
281
282    #[test]
283    fn append() {
284        let mut nested = Errors::new();
285        nested.append(vec![anyhow!("Generic error 1"), anyhow!("Generic error 2")]);
286
287        let mut errors = Errors::new();
288        errors.append(nested);
289        errors.append(anyhow!("Generic error 3"));
290
291        assert_eq!(errors.len(), 3);
292    }
293
294    #[test]
295    fn collect() {
296        let mut errors = Errors::new();
297
298        let result: Result<(), anyhow::Error> = Ok(());
299        assert_eq!(errors.collect(result), Some(()));
300
301        let result: Result<(), anyhow::Error> = Err(anyhow!("Generic error 1"));
302        assert_eq!(errors.collect(result), None);
303
304        assert_eq!(errors.len(), 1);
305    }
306
307    #[test]
308    fn collect_nested() {
309        let mut nested = Errors::new();
310        nested.push(anyhow!("Generic error 1"));
311        nested.push(anyhow!("Generic error 2"));
312        nested.push(anyhow!("Generic error 3"));
313
314        let mut errors = Errors::new();
315
316        let result: Result<(), Errors> = Err(nested);
317        assert_eq!(errors.collect(result), None);
318
319        assert_eq!(errors.len(), 3);
320    }
321
322    #[test]
323    fn fmt() {
324        let mut child = Errors::new();
325        child.push(anyhow!("Generic error 2"));
326        child.push(anyhow!("Generic error 3\nnew line"));
327        child.push(Errors(vec![anyhow!("Generic error 4")]));
328
329        let mut parent = Errors::new();
330        parent.push(child);
331        parent.push(io::Error::from_raw_os_error(1));
332
333        let mut errors = Errors::new();
334        errors.push(anyhow!("Generic error 1\nnew line"));
335        errors.push(parent);
336
337        assert_eq!(
338            format!("{errors:#}"),
339            "2 errors:
340                1. Generic error 1
341                   new line
342                2. 2 errors:
343                   1. 3 errors:
344                      1. Generic error 2
345                      2. Generic error 3
346                         new line
347                      3. Generic error 4
348                   2. Operation not permitted (os error 1)\n"
349                .replace("\n             ", "\n")
350        );
351    }
352}