error-collection 1.0.1

A generic collection around dynamic errors
Documentation
#![doc = include_str!("../README.md")]

use std::error::Error as StdError;
use std::fmt;

use derive_more::{Deref, DerefMut};

/// An explicit collection of Errors.
///
/// In order to be more concise, consider simply using [Errors].
pub type ErrorCollection = Errors;

/// A collection of multiple `anyhow::Error`s.
///
/// This is helpful because we often don't want to bail at the first error.
/// For example, take a simple method:
///
/// ```
/// # use anyhow::{bail, Result};
/// # #[derive(Debug, Clone, Copy)]
/// # struct Header { hash: u64 }
/// #
/// # fn read_header(raw: &[u8]) -> Result<Header> {
/// #     Ok(Header { hash: 0 })
/// # }
/// #
/// # fn hasher(contents: &str) -> u64 {
/// #     0
/// # }
/// #
/// fn check_file_integrity(raw: Vec<u8>) -> anyhow::Result<()> {
///   let Header { hash: expected_hash } = read_header(&raw[0..123])?;
///   let contents = str::from_utf8(&raw[123..])?;
///
///   if contents.len() < 2 {
///      bail!("Contents too short")
///   }
///
///   let data_hash = hasher(&contents);
///   if expected_hash != data_hash {
///      bail!("Header hash mismatch: {expected_hash} {data_hash}")
///   }
///
///   Ok(())
/// }
/// ```
///
/// We want the error to describe how the file is corrupted, but in our code we return
/// at the very first instance of an error. We are loosing valuable information during
/// runtime that could help debug a problem!
///
/// An [Errors] collection can help with this problem:
///
/// ```
/// # use anyhow::{anyhow, Result};
/// # use error_collection::Errors;
/// # #[derive(Debug, Clone, Copy)]
/// # struct Header { hash: u64 }
/// #
/// # fn read_header(raw: &[u8]) -> Result<Header> {
/// #     Ok(Header { hash: 0 })
/// # }
/// #
/// # fn hasher(contents: &str) -> u64 {
/// #     0
/// # }
/// #
/// fn check_file_integrity(raw: Vec<u8>) -> anyhow::Result<()> {
///   let mut errors = Errors::new();
///
///   // Convert the results to options
///   let header = errors.collect(read_header(&raw[0..123]));
///   let contents = errors.collect(str::from_utf8(&raw[123..]));
///
///   if let Some(contents) = contents && contents.len() < 2 {
///     errors.append("Contents too short");
///   }
///
///   if let Some(data_hash) = contents.map(hasher) &&
///      let Some(Header { hash }) = header &&
///      hash != data_hash {
///      errors.push(anyhow!("Header hash mismatch: {hash} {data_hash}"));
///   }
///
///   errors.as_result() // Ok(()) if there are no errors
/// }
/// ```
///
/// ```text
/// 2 errors:
///    1. Missing lightbulb
///    2. Camera needs film
/// ```
#[derive(Debug, Default, Deref, DerefMut)]
pub struct Errors(pub Vec<anyhow::Error>);

impl Errors {
    /// Creates an empty error collection.
    pub fn new() -> Self {
        Self::default()
    }

    /// Pushes an error to the back of this collection.
    ///
    /// Note: Pushing an Errors will nest that collection in this one.
    /// See [Self::append] for an alternative that avoids nesting.
    pub fn push(&mut self, err: impl Into<anyhow::Error>) {
        self.0.push(err.into());
    }

    /// Appends another collection of [Errors] to the back of this one.
    ///
    /// Tip: You can append from `Options` and `Results` that implement `Into<anyhow::Error>`.
    pub fn append(&mut self, err: impl Into<Self>) {
        self.0.append(&mut err.into().0);
    }

    /// Unwraps the error contained in this Result. Errors get "collected" into the collection, and out comes the optional result.
    pub fn collect<T, E>(&mut self, result: Result<T, E>) -> Option<T>
    where
        E: Into<anyhow::Error>,
    {
        match result.into() {
            Ok(value) => Some(value),
            Err(err) => {
                // Flatten out Errors
                self.append(err.into());
                None
            }
        }
    }

    /// Consumes the collection and returns the inner vector.
    pub fn into_vec(self) -> Vec<anyhow::Error> {
        self.0
    }

    /// Consumes the collection and returns a result.
    pub fn as_result(mut self) -> anyhow::Result<()> {
        match self.len() {
            0 => Ok(()),
            1 => Err(self.pop().unwrap()),
            _ => Err(self.into()),
        }
    }
}

const PADDING: usize = 3;

impl fmt::Display for Errors {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        format_errors(Ok(self), f, 0)
    }
}

fn format_errors(
    error: Result<&Errors, &anyhow::Error>,
    f: &mut fmt::Formatter<'_>,
    indent: usize,
) -> fmt::Result {
    match error {
        Err(error) if f.alternate() => write_padded(&format!("{:#}", error), f, indent),
        Err(error) => write_padded(&format!("{}", error), f, indent),
        Ok(errors) if errors.len() == 0 => writeln!(f, "none"),
        Ok(errors) if errors.len() == 1 => format_errors(Err(&errors[0]), f, indent),
        Ok(errors) => {
            writeln!(f, "{} errors:", errors.len())?;
            for (idx, err) in errors.iter().enumerate() {
                write!(f, "{}{}. ", " ".repeat(indent + PADDING), idx + 1)?;
                let error = err.downcast_ref::<Errors>().ok_or(err);
                format_errors(error, f, indent + PADDING)?;
            }

            Ok(())
        }
    }
}

fn spaces(padding: usize) -> &'static str {
    &"                                        "[..padding]
}

fn write_padded(string: &str, f: &mut fmt::Formatter<'_>, padding: usize) -> fmt::Result {
    let padding = spaces(padding + PADDING);
    for (idx, line) in string.split('\n').enumerate() {
        let padding = if idx == 0 { "" } else { &padding };
        writeln!(f, "{padding}{line}")?;
    }

    Ok(())
}

impl StdError for Errors {}

impl From<&str> for Errors {
    fn from(value: &str) -> Self {
        Self(vec![anyhow::anyhow!("{value}")])
    }
}

impl From<String> for Errors {
    fn from(value: String) -> Self {
        Self(vec![anyhow::anyhow!(value)])
    }
}

impl<T> From<Option<T>> for Errors
where
    T: Into<anyhow::Error>,
{
    fn from(result: Option<T>) -> Self {
        match result {
            Some(err) => err.into().into(),
            None => Self::default(),
        }
    }
}

impl<T, E> From<Result<T, E>> for Errors
where
    E: Into<anyhow::Error>,
{
    fn from(result: Result<T, E>) -> Self {
        match result {
            Ok(_) => Self::default(),
            Err(err) => err.into().into(),
        }
    }
}

impl From<Vec<anyhow::Error>> for Errors {
    fn from(errors: Vec<anyhow::Error>) -> Self {
        Self(errors)
    }
}

impl From<anyhow::Error> for Errors {
    fn from(error: anyhow::Error) -> Self {
        match error.downcast::<Self>() {
            Ok(errors) => errors,
            Err(error) => Self(vec![error]),
        }
    }
}

impl<T> From<Errors> for anyhow::Result<T>
where
    T: Default,
{
    fn from(mut value: Errors) -> Self {
        match value.len() {
            0 => Ok(T::default()),
            1 => Err(value.pop().unwrap()),
            _ => Err(value.into()),
        }
    }
}

#[cfg(test)]
mod tests {
    use std::io;

    use anyhow::anyhow;

    use super::*;

    #[test]
    fn push() {
        let mut nested = Errors::new();
        nested.push(anyhow!("Generic error 1"));
        nested.push(anyhow!("Generic error 2"));
        nested.push(anyhow!("Generic error 3"));

        let mut errors = Errors::new();
        errors.push(nested);
        errors.push(anyhow!("Generic error 4"));
        errors.push(io::Error::from_raw_os_error(22));

        assert_eq!(errors.len(), 3);
    }

    #[test]
    fn append() {
        let mut nested = Errors::new();
        nested.append(vec![anyhow!("Generic error 1"), anyhow!("Generic error 2")]);

        let mut errors = Errors::new();
        errors.append(nested);
        errors.append(anyhow!("Generic error 3"));

        assert_eq!(errors.len(), 3);
    }

    #[test]
    fn collect() {
        let mut errors = Errors::new();

        let result: Result<(), anyhow::Error> = Ok(());
        assert_eq!(errors.collect(result), Some(()));

        let result: Result<(), anyhow::Error> = Err(anyhow!("Generic error 1"));
        assert_eq!(errors.collect(result), None);

        assert_eq!(errors.len(), 1);
    }

    #[test]
    fn collect_nested() {
        let mut nested = Errors::new();
        nested.push(anyhow!("Generic error 1"));
        nested.push(anyhow!("Generic error 2"));
        nested.push(anyhow!("Generic error 3"));

        let mut errors = Errors::new();

        let result: Result<(), Errors> = Err(nested);
        assert_eq!(errors.collect(result), None);

        assert_eq!(errors.len(), 3);
    }

    #[test]
    fn fmt() {
        let mut child = Errors::new();
        child.push(anyhow!("Generic error 2"));
        child.push(anyhow!("Generic error 3\nnew line"));
        child.push(Errors(vec![anyhow!("Generic error 4")]));

        let mut parent = Errors::new();
        parent.push(child);
        parent.push(io::Error::from_raw_os_error(1));

        let mut errors = Errors::new();
        errors.push(anyhow!("Generic error 1\nnew line"));
        errors.push(parent);

        assert_eq!(
            format!("{errors:#}"),
            "2 errors:
                1. Generic error 1
                   new line
                2. 2 errors:
                   1. 3 errors:
                      1. Generic error 2
                      2. Generic error 3
                         new line
                      3. Generic error 4
                   2. Operation not permitted (os error 1)\n"
                .replace("\n             ", "\n")
        );
    }
}