Repository: shepmaster/snafu (Rust)

Issue:
Implement `iter_chain` on ErrorCompat
Copy over `iter_chain` from Rust unstable into `ErrorCompat`

Related: https://github.com/shepmaster/snafu/pull/186#discussion_r333283269
Depended by: https://github.com/shepmaster/snafu/pull/186


Discussion:
I would like to assist in this, as this seems to be the main blocker towards an error reporting abstraction for SNAFU.

The error handling working group has recently decided([see comment](https://github.com/rust-lang/project-error-handling/issues/27#issuecomment-763950178)) on an error design guideline.

> **Return source errors via `Error::source` unless the source error's message is included in your own error message in `Display`.**

And SNAFU sits in a tricky situation, due to how it is natural for source errors constructed via context selectors to also become available via `Error::source`. (I am not sure if there is a way to override this, although this feels hardly desirable.)

- If your error prints the source on `Display`, the chain of messages will be included in the error message while _also_ returning the source errors via `Error::source`, which goes against this guideline.
- If your error does not print `Display`, we're good with the guideline, but SNAFU does not provide an error reporting mechanism that takes the full chain of errors into account, hence requiring users to copy their error reporting boilerplate.

I am not entirely sure of how one should go about this in the project, but I could take an attempt and let it go under review as a PR.



Patch:
diff --git a/src/error_chain.rs b/src/error_chain.rs
new file mode 100644
index 00000000..51616e83
--- /dev/null
+++ b/src/error_chain.rs
@@ -0,0 +1,28 @@
+/// Backported version of the [`Chain`](std::error::Chain) struct,
+/// to versions of Rust lacking it.
+///
+/// Can be created via [`ErrorCompat::iter_chain`][crate::ErrorCompat::iter_chain].
+pub struct ChainCompat<'a> {
+    inner: Option<&'a dyn std::error::Error>,
+}
+
+impl<'a> ChainCompat<'a> {
+    /// Creates a new error chain iterator.
+    pub fn new(error: &'a dyn std::error::Error) -> Self {
+        ChainCompat { inner: Some(error) }
+    }
+}
+
+impl<'a> Iterator for ChainCompat<'a> {
+    type Item = &'a dyn std::error::Error;
+
+    fn next(&mut self) -> Option<Self::Item> {
+        match self.inner {
+            None => None,
+            Some(e) => {
+                self.inner = e.source();
+                Some(e)
+            }
+        }
+    }
+}
diff --git a/src/lib.rs b/src/lib.rs
index 86928cce..d052653d 100644
--- a/src/lib.rs
+++ b/src/lib.rs
@@ -226,6 +226,11 @@ pub use std::backtrace::Backtrace;
 #[cfg(feature = "futures")]
 pub mod futures;
 
+#[cfg(feature = "std")]
+mod error_chain;
+#[cfg(feature = "std")]
+pub use crate::error_chain::*;
+
 doc_comment::doc_comment! {
     include_str!("Snafu.md"),
     pub use snafu_derive::Snafu;
@@ -883,6 +888,20 @@ pub trait ErrorCompat {
     fn backtrace(&self) -> Option<&Backtrace> {
         None
     }
+
+    /// Returns an iterator for traversing the chain of errors,
+    /// starting with the current error
+    /// and continuing with recursive calls to `Error::source`.
+    ///
+    /// To omit the current error and only traverse its sources,
+    /// use `skip(1)`.
+    #[cfg(feature = "std")]
+    fn iter_chain(&self) -> ChainCompat
+    where
+        Self: AsErrorSource,
+    {
+        ChainCompat::new(self.as_error_source())
+    }
 }
 
 impl<'a, E> ErrorCompat for &'a E


Test patch:
diff --git a/tests/error_chain.rs b/tests/error_chain.rs
new file mode 100644
index 00000000..30f7fabd
--- /dev/null
+++ b/tests/error_chain.rs
@@ -0,0 +1,57 @@
+use snafu::{ChainCompat, Snafu};
+use std::fmt::Debug;
+
+#[derive(Debug, Clone, Snafu)]
+enum LeafError {
+    #[snafu(display("User ID {} is invalid", user_id))]
+    InvalidUser { user_id: i32 },
+    #[snafu(display("no user available"))]
+    MissingUser,
+}
+
+#[derive(Debug, Clone, Snafu)]
+enum MiddleError {
+    #[snafu(display("failed to check the user"))]
+    CheckUser { source: LeafError },
+}
+
+#[derive(Debug, Clone, Snafu)]
+enum Error {
+    #[snafu(display("access control failure"))]
+    AccessControl { source: MiddleError },
+}
+
+#[track_caller]
+fn assert_eq_debug(a: impl Debug, b: impl Debug) {
+    assert_eq!(format!("{:?}", a), format!("{:?}", b));
+}
+
+#[test]
+fn chain_compat_iterates() {
+    use snafu::IntoError;
+
+    let bottom_error = InvalidUserSnafu { user_id: 12 }.build();
+    let middle_error = CheckUserSnafu.into_error(bottom_error.clone());
+    let error = AccessControlSnafu.into_error(middle_error.clone());
+
+    let errors: Vec<_> = ChainCompat::new(&error).collect();
+
+    assert_eq_debug(&errors[0], &error);
+    assert_eq_debug(&errors[1], &middle_error);
+    assert_eq_debug(&errors[2], &bottom_error);
+}
+
+#[test]
+fn errorcompat_chain_iterates() {
+    use snafu::{ErrorCompat, IntoError};
+
+    let bottom_error = InvalidUserSnafu { user_id: 12 }.build();
+    let middle_error = CheckUserSnafu.into_error(bottom_error.clone());
+    let error = AccessControlSnafu.into_error(middle_error.clone());
+
+    let errors: Vec<_> = ErrorCompat::iter_chain(&error).collect();
+
+    assert_eq_debug(&errors[0], &error);
+    assert_eq_debug(&errors[1], &middle_error);
+    assert_eq_debug(&errors[2], &bottom_error);
+}