liberr 0.1.0

A flexible error message handling crate focused on error tracing for useful debugging
Documentation
use std::{
    borrow::Cow,
    error::Error,
    fmt::{Debug, Display, Write},
    panic::Location,
};

/// Allows tracking [Location] with the context of whether the [LibErr]
/// was created at a certain location, or if it simply wrapped an error
/// at a certain location.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
enum ErrorLoc {
    /// Singifies the [LibErr] was created at the given [Location]
    CreatedAt(Location<'static>),
    /// Singifies the [LibErr] wrapped a given [Error] at [Location]
    WrappedAt(Location<'static>),
}

impl AsRef<Location<'static>> for ErrorLoc {
    #[inline]
    fn as_ref(&self) -> &Location<'static> {
        match self {
            Self::CreatedAt(loc) => loc,
            Self::WrappedAt(loc) => loc,
        }
    }
}

impl Display for ErrorLoc {
    #[inline]
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            Self::CreatedAt(loc) => write!(f, "Created@{}", loc),
            Self::WrappedAt(loc) => write!(f, "Wrapped@{}", loc),
        }
    }
}

/// [LibErr] is an [Error] type that allows tracking where in the
/// codebase the error occured, while building a trace.
#[derive(Clone)]
pub struct LibErr {
    /// The best location in the code to trace the [LibErr]
    loc: ErrorLoc,
    /// A message to be displayed
    msg: Cow<'static, str>,
    /// The cause of [LibErr]
    src: Option<Box<LibErr>>,
}

impl LibErr {
    /// Returns the closest [Location] in the code as possible that
    /// is relevant to this [LibErr]
    #[inline]
    pub fn loc(&self) -> &Location<'static> {
        self.loc.as_ref()
    }

    /// Returns a [bool] depending if this [LibErr] has a source (aka a cause)
    #[inline]
    pub fn has_source(&self) -> bool {
        self.src.is_some()
    }
}

impl Debug for LibErr {
    #[inline]
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        writeln!(f, "\n{}", self)?;
        let sources = self.iter_sources();

        // If there's any sizes
        if let (1, _) = sources.size_hint() {
            for src in sources {
                if src.has_source() {
                    writeln!(f, "-> {}", src)?;
                } else {
                    writeln!(f, "\\-> {}", src)?;
                }
            }
            f.write_char('\n')?;
        }
        Ok(())
    }
}

impl Display for LibErr {
    #[inline]
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}\t-> {}", self.loc, self.msg)?;
        Ok(())
    }
}

impl Error for LibErr {
    #[inline]
    fn description(&self) -> &str {
        &self.msg
    }

    #[inline]
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match &self.src {
            Some(src) => Some(src.as_ref()),
            None => None,
        }
    }
}

unsafe impl Send for LibErr {}

impl LibErr {
    /// Creates a new [LibErr]
    #[inline]
    #[track_caller]
    pub fn new<T: ToString>(msg: T) -> Self {
        Self {
            loc: ErrorLoc::CreatedAt(*Location::caller()),
            msg: Cow::Owned(msg.to_string()),
            src: None,
        }
    }

    /// Creates a new [LibErr] given a static string.  If
    /// string is static, this is better than [LibErr::new]
    #[inline]
    #[track_caller]
    pub const fn new_static(msg: &'static str) -> Self {
        Self {
            loc: ErrorLoc::CreatedAt(*Location::caller()),
            msg: Cow::Borrowed(msg),
            src: None,
        }
    }

    /// Allows accessing the [LibErr] source {
    #[inline]
    pub fn get_source(&self) -> Option<&LibErr> {
        self.src.as_ref().map(AsRef::as_ref)
    }

    /// Wrap anything implementing [Error] into a [LibErr].
    ///
    /// Not ideal if it is a [LibErr] as some information may be lost.
    #[inline]
    #[track_caller]
    pub fn from_err<E: Error>(err: E) -> Self {
        Self {
            msg: Cow::Owned(err.to_string()),
            loc: ErrorLoc::WrappedAt(*Location::caller()),
            src: err.source().map(|err| Box::new(Self::from_err(err))),
        }
    }

    /// Iterate through the sources, where the first value returned is the [LibErr]
    /// that caused this [LibErr], then move up the trace where the last [LibErr]
    /// is the first [LibErr] to start it
    #[inline]
    pub fn iter_sources(&self) -> TraceIter<'_> {
        TraceIter { cur: self }
    }

    /// Allows creating a new [LibErr] where `self` becomes the
    /// source of the new [LibErr].  Allows adding additional context
    /// and building a proper trace.
    #[inline]
    #[track_caller]
    pub fn derive_err<T: ToString>(self, msg: T) -> Self {
        Self {
            msg: Cow::Owned(msg.to_string()),
            loc: ErrorLoc::CreatedAt(*Location::caller()),
            src: Some(Box::new(self)),
        }
    }

    #[inline]
    pub fn write_trace<F: std::fmt::Write>(&self, f: &mut F) -> std::fmt::Result {
        for err in self.iter_sources() {
            writeln!(f, "{}", err)?;
        }
        Ok(())
    }
}

/// [TraceIter] allows iterating down trace of a [LibErr] by iterating
/// through the sources.  This means the first element will be the most
/// outter [LibErr], where the next [LibErr] is to be the source (or
/// cause) of the previous [LibErr]
#[derive(Debug, Clone)]
pub struct TraceIter<'a> {
    cur: &'a LibErr,
}

impl<'a> Iterator for TraceIter<'a> {
    type Item = &'a LibErr;
    #[inline]
    fn next(&mut self) -> Option<Self::Item> {
        let src = self.cur.get_source()?;
        self.cur = src;
        Some(src)
    }

    #[inline]
    fn size_hint(&self) -> (usize, Option<usize>) {
        if self.cur.has_source() {
            (1, None)
        } else {
            (0, Some(0))
        }
    }
}

#[macro_export]
macro_rules! create_err {
    ($msg: literal) => {
        $crate::LibErr::new_static(concat!($msg))
    };
    ($msg: expr) => {
        $crate::LibErr::new($msg)
    };
    ($err: expr, $msg: expr) => {
        $crate::LibErr::derive_err($crate::LibErr::from($err), $msg)
    };
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_new_error() {
        let err = LibErr::new("Test error");
        assert_eq!(err.msg, "Test error");
        assert!(matches!(err.loc, ErrorLoc::CreatedAt(_)));
        assert!(err.get_source().is_none());
    }

    #[test]
    fn test_new_static_error() {
        let err = LibErr::new_static("Static error");
        assert_eq!(err.msg, "Static error");
        assert!(matches!(err.loc, ErrorLoc::CreatedAt(_)));
        assert!(err.get_source().is_none());
    }

    #[test]
    fn test_from_err_wraps_error() {
        let base_err = std::io::Error::new(std::io::ErrorKind::Other, "IO failure");
        let wrapped = LibErr::from_err(base_err);

        assert_eq!(wrapped.msg, "IO failure");
        assert!(matches!(wrapped.loc, ErrorLoc::WrappedAt(_)));
        assert!(wrapped.get_source().is_none());
    }

    #[test]
    fn test_wraps_another_crateerr() {
        let base_err = LibErr::new("Base error");
        let wrapped = base_err.derive_err("Additional context");

        assert_eq!(wrapped.msg, "Additional context");
        assert!(wrapped.get_source().is_some());
        assert_eq!(wrapped.get_source().unwrap().msg, "Base error");
    }

    #[test]
    fn test_iter_sources() {
        let base_err = LibErr::new("Base error");
        let mid_err = base_err.derive_err("Mid error");
        let top_err = mid_err.derive_err("Top error");

        let mut iter = top_err.iter_sources();
        assert_eq!(iter.next().unwrap().msg, "Mid error");
        assert_eq!(iter.next().unwrap().msg, "Base error");
        assert!(iter.next().is_none());
    }

    #[test]
    fn test_macro_create_err() {
        let err1 = create_err!("Static macro error");
        let err2 = create_err!("Dynamic macro error".to_string());

        assert_eq!(err1.msg, "Static macro error");
        assert_eq!(err2.msg, "Dynamic macro error");
    }

    #[test]
    fn test_macro_create_err_with_chaining() {
        let base_err = create_err!("Initial error");
        let derived_err = create_err!(base_err, "Derived error");

        assert_eq!(derived_err.msg, "Derived error");
        assert!(derived_err.get_source().is_some());
        assert_eq!(derived_err.get_source().unwrap().msg, "Initial error");
    }

    #[test]
    fn test_error_loc_tracking() {
        let err1 = LibErr::new("Error location test");
        let err2 = LibErr::from_err(err1.clone());

        assert!(matches!(err1.loc, ErrorLoc::CreatedAt(_)));
        assert!(matches!(err2.loc, ErrorLoc::WrappedAt(_)));
    }
}