Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add compatibility layer for eyre and anyhow #763

Merged
merged 14 commits into from
Jul 8, 2022
Merged
4 changes: 3 additions & 1 deletion packages/libs/error-stack/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ tracing-error = { version = "0.2.0", optional = true }
once_cell = { version = "1.10.0", optional = true }
pin-project = { version = "1.0.10", optional = true }
futures-core = { version = "0.3.21", optional = true, default-features = false }
anyhow = { version = "1.0.58", default-features = false, optional = true }
eyre = { version = "0.6.8", default-features = false, optional = true }

[dev-dependencies]
serde = { version = "1.0.137", features = ["derive"] }
Expand All @@ -28,7 +30,7 @@ rustc_version = "0.2.3"

[features]
default = ["std"]
std = []
std = ["anyhow?/std"]
hooks = ["dep:once_cell", "std"]
spantrace = ["dep:tracing-error"]
futures = ["dep:pin-project", "dep:futures-core"]
Expand Down
6 changes: 3 additions & 3 deletions packages/libs/error-stack/Makefile.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
extend = { path = "../../../.github/scripts/rust/Makefile.toml" }

[env]
CARGO_CLIPPY_HACK_FLAGS = "--workspace --feature-powerset --exclude-features default"
CARGO_TEST_HACK_FLAGS = "--workspace --feature-powerset --exclude-features default"
CARGO_MIRI_HACK_FLAGS = "--workspace --feature-powerset --exclude-features default"
CARGO_CLIPPY_HACK_FLAGS = "--workspace --feature-powerset --exclude-features default --optional-deps eyre,anyhow"
CARGO_TEST_HACK_FLAGS = "--workspace --feature-powerset --exclude-features default --optional-deps eyre,anyhow"
CARGO_MIRI_HACK_FLAGS = "--workspace --feature-powerset --exclude-features default --optional-deps eyre,anyhow"
101 changes: 101 additions & 0 deletions packages/libs/error-stack/src/compat/anyhow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
#[cfg(nightly)]
use core::any::Demand;
use core::{fmt, panic::Location};
#[cfg(all(nightly, feature = "std"))]
use std::{backtrace::Backtrace, error::Error};

use anyhow::Error as AnyhowError;

use crate::{Context, Frame, IntoReportCompat, Report, Result};

#[repr(transparent)]
struct AnyhowContext(AnyhowError);

impl fmt::Debug for AnyhowContext {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, fmt)
}
}

impl fmt::Display for AnyhowContext {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, fmt)
}
}

impl Context for AnyhowContext {
#[cfg(nightly)]
fn provide<'a>(&'a self, demand: &mut Demand<'a>) {
demand.provide_ref(&self.0);
#[cfg(feature = "std")]
if let Some(backtrace) = self.0.chain().find_map(Error::backtrace) {
demand.provide_ref(backtrace);
}
}
}

impl<T> IntoReportCompat for core::result::Result<T, AnyhowError> {
type Err = AnyhowError;
type Ok = T;

#[track_caller]
fn into_report(self) -> Result<T, AnyhowError> {
match self {
Ok(t) => Ok(t),
Err(anyhow) => {
#[cfg(feature = "std")]
let sources = anyhow
.chain()
.skip(1)
.map(ToString::to_string)
.collect::<Vec<_>>();

#[cfg(all(nightly, feature = "std"))]
let backtrace = anyhow
.chain()
.all(|error| error.backtrace().is_none())
.then(Backtrace::capture);
Comment on lines +53 to +57
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be removed as part of #747


#[cfg_attr(not(feature = "std"), allow(unused_mut))]
let mut report = Report::from_frame(
Frame::from_compat::<AnyhowError, AnyhowContext>(
AnyhowContext(anyhow),
Location::caller(),
),
#[cfg(all(nightly, feature = "std"))]
backtrace,
#[cfg(feature = "spantrace")]
Some(tracing_error::SpanTrace::capture()),
);

#[cfg(feature = "std")]
for source in sources {
report = report.attach_printable(source);
}

Err(report)
}
}
}
}

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

use crate::{test_helper::messages, IntoReportCompat};

#[test]
fn conversion() {
let anyhow: Result<(), _> = Err(anyhow!("A").context("B").context("C"));

let report = anyhow.into_report().unwrap_err();
#[cfg(feature = "std")]
let expected_output = ["A", "B", "C"];
#[cfg(not(feature = "std"))]
let expected_output = ["C"];
for (anyhow, expected) in messages(&report).into_iter().zip(expected_output) {
assert_eq!(anyhow, expected);
}
}
}
103 changes: 103 additions & 0 deletions packages/libs/error-stack/src/compat/eyre.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#[cfg(nightly)]
use core::any::Demand;
use core::{fmt, panic::Location};
#[cfg(all(nightly, feature = "std"))]
use std::{backtrace::Backtrace, error::Error};

use eyre::Report as EyreReport;

use crate::{Context, Frame, IntoReportCompat, Report, Result};

#[repr(transparent)]
struct EyreContext(EyreReport);

impl fmt::Debug for EyreContext {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, fmt)
}
}

impl fmt::Display for EyreContext {
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, fmt)
}
}

impl Context for EyreContext {
#[cfg(nightly)]
fn provide<'a>(&'a self, demand: &mut Demand<'a>) {
demand.provide_ref(&self.0);
#[cfg(feature = "std")]
if let Some(backtrace) = self.0.chain().find_map(Error::backtrace) {
demand.provide_ref(backtrace);
}
}
}

impl<T> IntoReportCompat for core::result::Result<T, EyreReport> {
type Err = EyreReport;
type Ok = T;

#[track_caller]
fn into_report(self) -> Result<T, EyreReport> {
match self {
Ok(t) => Ok(t),
Err(eyre) => {
let sources = eyre
.chain()
.skip(1)
.map(alloc::string::ToString::to_string)
.collect::<alloc::vec::Vec<_>>();

#[cfg(all(nightly, feature = "std"))]
let backtrace = eyre
.chain()
.all(|error| error.backtrace().is_none())
.then(Backtrace::capture);

let mut report = Report::from_frame(
Frame::from_compat::<EyreReport, EyreContext>(
EyreContext(eyre),
Location::caller(),
),
#[cfg(all(nightly, feature = "std"))]
backtrace,
#[cfg(feature = "spantrace")]
Some(tracing_error::SpanTrace::capture()),
);

for source in sources {
report = report.attach_printable(source);
}

Err(report)
}
}
}
}

#[cfg(test)]
mod tests {
use alloc::boxed::Box;

use eyre::eyre;

use crate::{test_helper::messages, IntoReportCompat};

#[test]
#[cfg_attr(
miri,
ignore = "bug: miri is failing for `eyre`, this is unrelated to our implementation"
)]
fn conversion() {
eyre::set_hook(Box::new(eyre::DefaultHandler::default_with)).expect("Could not set hook");

let eyre: Result<(), _> = Err(eyre!("A").wrap_err("B").wrap_err("C"));

let report = eyre.into_report().unwrap_err();
let expected_output = ["A", "B", "C"];
for (eyre, expected) in messages(&report).into_iter().zip(expected_output) {
assert_eq!(eyre, expected);
}
}
}
31 changes: 31 additions & 0 deletions packages/libs/error-stack/src/compat/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#[cfg(feature = "anyhow")]
mod anyhow;
#[cfg(feature = "eyre")]
mod eyre;

/// Compatibility trait to convert from external libraries to [`Report`].
TimDiekmann marked this conversation as resolved.
Show resolved Hide resolved
///
/// *Note*: It's not possible to implement [`IntoReport`] or [`Context`] on other error libraries'
/// types as both traits have blanket implementation relying on [`Error`]. Thus, implementing either
/// trait would violate the orphan rule; the upstream crate could implement [`Error`] and this would
/// imply an implementation for [`IntoReport`]/[`Context`].
///
/// [`Report`]: crate::Report
/// [`IntoReport`]: crate::IntoReport
/// [`Context`]: crate::Context
/// [`Error`]: std::error::Error
#[cfg(any(feature = "anyhow", feature = "eyre"))]
pub trait IntoReportCompat: Sized {
/// Type of the [`Ok`] value in the [`Result`]
type Ok;

/// Type of the resulting [`Err`] variant wrapped inside a [`Report<E>`].
///
/// [`Report<E>`]: crate::Report
type Err;

/// Converts the [`Err`] variant of the [`Result`] to a [`Report`]
///
/// [`Report`]: crate::Report
fn into_report(self) -> crate::Result<Self::Ok, Self::Err>;
}
12 changes: 12 additions & 0 deletions packages/libs/error-stack/src/frame/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,18 @@ impl Frame {
)
}

/// Crates a frame from [`anyhow::Error`].
#[cfg(any(feature = "anyhow", feature = "eyre"))]
pub(crate) fn from_compat<T, C: Context>(
compat: C,
location: &'static Location<'static>,
) -> Self
where
T: fmt::Display + fmt::Debug + Send + Sync + 'static,
{
Self::from_unerased(compat, location, None, VTable::new_compat::<T, C>())
}

fn vtable(&self) -> &'static VTable {
// SAFETY: Use vtable to attach the frames' native vtable for the right original type.
unsafe { self.erased_frame.as_ref().vtable() }
Expand Down
17 changes: 17 additions & 0 deletions packages/libs/error-stack/src/frame/vtable.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,23 @@ impl VTable {
}
}

/// Creates a `VTable` for a [`compat`].
///
/// [`Compat`]: crate::Compat
#[cfg(any(feature = "anyhow", feature = "eyre"))]
pub fn new_compat<T, C: Context>() -> &'static Self
where
T: fmt::Display + fmt::Debug + Send + Sync + 'static,
{
&Self {
object_drop: Self::object_drop::<C>,
object_downcast: Self::object_downcast::<T>,
unerase: Self::unerase_context::<C>,
#[cfg(nightly)]
provide: Self::context_provide::<C>,
}
}

/// Unerases the `frame` as a [`FrameKind`].
pub(in crate::frame) fn unerase<'f>(&self, frame: &'f NonNull<ErasableFrame>) -> FrameKind<'f> {
// SAFETY: Use vtable to attach the frames' native vtable for the right original type.
Expand Down
5 changes: 5 additions & 0 deletions packages/libs/error-stack/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,6 +357,8 @@
//! `hooks` |Enables the usage of [`set_display_hook`] and [`set_debug_hook`]| `std` | disabled
//! `spantrace`| Enables the capturing of [`SpanTrace`]s | | disabled
//! `futures` | Provides a [`FutureExt`] adaptor | | disabled
//! `anyhow` | Provides conversion from [`anyhow::Error`] to [`Report`] | | disabled
//! `eyre` | Provides conversion from [`eyre::Report`] to [`Report`] | | disabled
//!
//! [`set_display_hook`]: Report::set_display_hook
//! [`set_debug_hook`]: Report::set_debug_hook
Expand All @@ -383,6 +385,7 @@

extern crate alloc;

pub(crate) mod compat;
mod frame;
pub mod iter;
mod macros;
Expand All @@ -395,6 +398,8 @@ mod hook;
#[cfg(test)]
pub(crate) mod test_helper;

#[cfg(any(feature = "anyhow", feature = "eyre"))]
pub use self::compat::IntoReportCompat;
#[doc(inline)]
pub use self::ext::*;
#[cfg(feature = "hooks")]
Expand Down