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(Debug, 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::Display for Errors {
159    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
160        format_collection(f, self, 0)
161    }
162}
163
164/// Custom formatter for Errors
165fn format_collection(f: &mut fmt::Formatter<'_>, errors: &Errors, indent: usize) -> fmt::Result {
166    if errors.is_empty() {
167        writeln!(f, "none")
168    } else if errors.len() == 1 {
169        format_error(f, &errors[0], indent)
170    } else {
171        writeln!(f, "{} errors:", errors.len())?;
172        for (idx, error) in errors.iter().enumerate() {
173            write!(f, "{}{}. ", spaces(indent + PADDING), idx + 1)?;
174            match error.downcast_ref::<Errors>() {
175                None => format_error(f, error, indent + PADDING)?,
176                Some(errors) => format_collection(f, errors, indent + PADDING)?,
177            }
178        }
179        Ok(())
180    }
181}
182
183/// Custom formatter for an anyhow::Error nested in a collection
184fn format_error(f: &mut fmt::Formatter<'_>, error: &anyhow::Error, indent: usize) -> fmt::Result {
185    let padding = spaces(indent + PADDING);
186    let error_string = if f.alternate() {
187        format!("{:#}", error)
188    } else {
189        format!("{}", error)
190    };
191    for (idx, line) in error_string.split('\n').enumerate() {
192        let padding = if idx == 0 { "" } else { padding };
193        writeln!(f, "{padding}{line}")?;
194    }
195    Ok(())
196}
197
198/// Zero-alloc version of " ".repeat(x)
199fn spaces(indent: usize) -> &'static str {
200    &"                                "[..indent.min(32)]
201}
202
203impl StdError for Errors {}
204
205impl From<&str> for Errors {
206    fn from(value: &str) -> Self {
207        Self(vec![anyhow::anyhow!("{value}")])
208    }
209}
210
211impl From<String> for Errors {
212    fn from(value: String) -> Self {
213        Self(vec![anyhow::anyhow!(value)])
214    }
215}
216
217impl<T> From<Option<T>> for Errors
218where
219    T: Into<anyhow::Error>,
220{
221    fn from(result: Option<T>) -> Self {
222        match result {
223            Some(err) => err.into().into(),
224            None => Self::default(),
225        }
226    }
227}
228
229impl<T, E> From<Result<T, E>> for Errors
230where
231    E: Into<anyhow::Error>,
232{
233    fn from(result: Result<T, E>) -> Self {
234        match result {
235            Ok(_) => Self::default(),
236            Err(err) => err.into().into(),
237        }
238    }
239}
240
241impl From<Vec<anyhow::Error>> for Errors {
242    fn from(errors: Vec<anyhow::Error>) -> Self {
243        Self(errors)
244    }
245}
246
247impl From<anyhow::Error> for Errors {
248    fn from(error: anyhow::Error) -> Self {
249        match error.downcast::<Self>() {
250            Ok(errors) => errors,
251            Err(error) => Self(vec![error]),
252        }
253    }
254}
255
256impl<T> From<Errors> for anyhow::Result<T>
257where
258    T: Default,
259{
260    fn from(mut value: Errors) -> Self {
261        match value.len() {
262            0 => Ok(T::default()),
263            1 => Err(value.pop().unwrap()),
264            _ => Err(value.into()),
265        }
266    }
267}
268
269#[cfg(test)]
270mod tests {
271    use std::io;
272
273    use anyhow::anyhow;
274
275    use super::*;
276
277    #[test]
278    fn push() {
279        let mut nested = Errors::new();
280        nested.push(anyhow!("Generic error 1"));
281        nested.push(anyhow!("Generic error 2"));
282        nested.push(anyhow!("Generic error 3"));
283
284        let mut errors = Errors::new();
285        errors.push(nested);
286        errors.push(anyhow!("Generic error 4"));
287        errors.push(io::Error::from_raw_os_error(22));
288
289        assert_eq!(errors.len(), 3);
290    }
291
292    #[test]
293    fn append() {
294        let mut nested = Errors::new();
295        nested.append(vec![anyhow!("Generic error 1"), anyhow!("Generic error 2")]);
296
297        let mut errors = Errors::new();
298        errors.append(nested);
299        errors.append(anyhow!("Generic error 3"));
300
301        assert_eq!(errors.len(), 3);
302    }
303
304    #[test]
305    fn collect() {
306        let mut errors = Errors::new();
307
308        let result: Result<(), anyhow::Error> = Ok(());
309        assert_eq!(errors.collect(result), Some(()));
310
311        let result: Result<(), anyhow::Error> = Err(anyhow!("Generic error 1"));
312        assert_eq!(errors.collect(result), None);
313
314        assert_eq!(errors.len(), 1);
315    }
316
317    #[test]
318    fn collect_nested() {
319        let mut nested = Errors::new();
320        nested.push(anyhow!("Generic error 1"));
321        nested.push(anyhow!("Generic error 2"));
322        nested.push(anyhow!("Generic error 3"));
323
324        let mut errors = Errors::new();
325
326        let result: Result<(), Errors> = Err(nested);
327        assert_eq!(errors.collect(result), None);
328
329        assert_eq!(errors.len(), 3);
330    }
331
332    #[test]
333    fn fmt() {
334        let mut child = Errors::new();
335        child.push(anyhow!("Generic error 2"));
336        child.push(anyhow!("Generic error 3\nnew line"));
337        child.push(Errors(vec![anyhow!("Generic error 4")]));
338
339        let mut parent = Errors::new();
340        parent.push(child);
341        parent.push(io::Error::from_raw_os_error(1));
342
343        let mut errors = Errors::new();
344        errors.push(anyhow!("Generic error 1\nnew line"));
345        errors.push(parent);
346
347        assert_eq!(
348            format!("{errors:#}"),
349            "2 errors:
350                1. Generic error 1
351                   new line
352                2. 2 errors:
353                   1. 3 errors:
354                      1. Generic error 2
355                      2. Generic error 3
356                         new line
357                      3. Generic error 4
358                   2. Operation not permitted (os error 1)\n"
359                .replace("\n             ", "\n")
360        );
361    }
362}