use alloc::boxed::Box;
use alloc::string::String;
use core::error::Error as StdError;
use core::fmt;
use crate::{Backtrace, Capturable as _, Diagnostic, SpanTrace};
type BoxError = Box<dyn StdError + Send + Sync + 'static>;
#[must_use = "this `Welp` error should be returned or propagated, not discarded"]
pub struct Welp(WelpRepr);
enum WelpRepr {
Sourced {
message: Option<Box<str>>,
source: BoxError,
traces: Box<(Backtrace, SpanTrace)>,
location: &'static core::panic::Location<'static>,
},
Traced {
message: Box<str>,
traces: Box<(Backtrace, SpanTrace)>,
location: &'static core::panic::Location<'static>,
},
}
impl Welp {
#[track_caller]
pub fn new(message: impl Into<String>) -> Self {
Self(WelpRepr::Traced {
message: message.into().into_boxed_str(),
traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
location: core::panic::Location::caller(),
})
}
#[track_caller]
pub fn wrap<E>(source: E, message: impl Into<String>) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self(WelpRepr::Sourced {
message: Some(message.into().into_boxed_str()),
source: Box::new(source),
traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
location: core::panic::Location::caller(),
})
}
#[track_caller]
pub fn wrap_boxed(source: BoxError, message: impl Into<String>) -> Self {
Self(WelpRepr::Sourced {
message: Some(message.into().into_boxed_str()),
source,
traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
location: core::panic::Location::caller(),
})
}
#[track_caller]
pub fn from_error<E>(source: E) -> Self
where
E: StdError + Send + Sync + 'static,
{
Self(WelpRepr::Sourced {
message: None,
source: Box::new(source),
traces: Box::new((Backtrace::capture(), SpanTrace::capture())),
location: core::panic::Location::caller(),
})
}
#[must_use]
#[inline]
pub fn message(&self) -> Option<&str> {
match &self.0 {
WelpRepr::Sourced { message, .. } => message.as_deref(),
WelpRepr::Traced { message, .. } => Some(message),
}
}
#[must_use]
#[inline]
pub fn downcast_ref<E: StdError + 'static>(&self) -> Option<&E> {
match &self.0 {
WelpRepr::Sourced { source, .. } => source.downcast_ref::<E>(),
WelpRepr::Traced { .. } => None,
}
}
#[must_use]
#[inline]
pub fn downcast_mut<E: StdError + 'static>(&mut self) -> Option<&mut E> {
match &mut self.0 {
WelpRepr::Sourced { source, .. } => source.downcast_mut::<E>(),
WelpRepr::Traced { .. } => None,
}
}
pub fn downcast<E: StdError + 'static>(self) -> Result<E, Self> {
match self.0 {
WelpRepr::Sourced {
message,
source,
traces,
location,
} => match source.downcast::<E>() {
Ok(source) => Ok(*source),
Err(source) => Err(Self(WelpRepr::Sourced {
message,
source,
traces,
location,
})),
},
traced @ WelpRepr::Traced { .. } => Err(Self(traced)),
}
}
}
impl fmt::Debug for Welp {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let mut dbg = f.debug_struct("Welp");
if let Some(message) = self.message() {
dbg.field("message", &message);
}
match &self.0 {
WelpRepr::Sourced { source, .. } => {
dbg.field("source", source);
}
WelpRepr::Traced { .. } => {}
}
dbg.finish()
}
}
impl fmt::Display for Welp {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
WelpRepr::Sourced {
message: None,
source,
..
} => source.fmt(f),
WelpRepr::Sourced {
message: Some(message),
..
}
| WelpRepr::Traced { message, .. } => f.write_str(message),
}
}
}
impl StdError for Welp {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match &self.0 {
WelpRepr::Sourced { source, .. } => Some(&**source),
WelpRepr::Traced { .. } => None,
}
}
#[cfg(feature = "unstable-error-generic-member-access")]
fn provide<'a>(&'a self, request: &mut core::error::Request<'a>) {
let traces = match &self.0 {
WelpRepr::Sourced { source, traces, .. } => {
source.provide(request);
traces
}
WelpRepr::Traced { traces, .. } => traces,
};
if traces.0.is_captured() {
request.provide_ref::<Backtrace>(&traces.0);
}
if traces.1.is_captured() {
request.provide_ref::<SpanTrace>(&traces.1);
}
}
}
impl Diagnostic for Welp {
fn oopsie_backtrace(&self) -> Option<&Backtrace> {
match &self.0 {
WelpRepr::Sourced { source, traces, .. } => {
crate::__private::source_backtrace(&**source).or(Some(&traces.0))
}
WelpRepr::Traced { traces, .. } => Some(&traces.0),
}
}
fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
match &self.0 {
WelpRepr::Sourced { source, traces, .. } => {
crate::__private::source_spantrace(&**source).or(Some(&traces.1))
}
WelpRepr::Traced { traces, .. } => Some(&traces.1),
}
}
fn oopsie_location(&self) -> Option<&'static core::panic::Location<'static>> {
match &self.0 {
WelpRepr::Sourced { location, .. } | WelpRepr::Traced { location, .. } => {
Some(location)
}
}
}
fn oopsie_exit_code(&self) -> Option<core::num::NonZeroU8> {
match &self.0 {
WelpRepr::Sourced { source, .. } => crate::__private::source_exit_code(&**source),
WelpRepr::Traced { .. } => None,
}
}
}
pub trait WelpResultExt<T, E>: Sized {
fn welp(self) -> Result<T, Welp>;
fn welp_context(self, message: impl Into<String>) -> Result<T, Welp>;
fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
where
S: Into<String>,
F: FnOnce(&E) -> S;
}
impl<T, E> WelpResultExt<T, E> for Result<T, E>
where
E: StdError + Send + Sync + 'static,
{
#[inline]
#[track_caller]
fn welp(self) -> Result<T, Welp> {
match self {
Ok(value) => Ok(value),
Err(e) => Err(Welp::from_error(e)),
}
}
#[inline]
#[track_caller]
fn welp_context(self, message: impl Into<String>) -> Result<T, Welp> {
match self {
Ok(value) => Ok(value),
Err(e) => Err(Welp::wrap(e, message)),
}
}
#[inline]
#[track_caller]
fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
where
S: Into<String>,
F: FnOnce(&E) -> S,
{
match self {
Ok(value) => Ok(value),
Err(e) => {
let msg = f(&e);
Err(Welp::wrap(e, msg))
}
}
}
}
pub trait WelpOptionExt<T>: Sized {
fn welp_context(self, message: impl Into<String>) -> Result<T, Welp>;
fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
where
S: Into<String>,
F: FnOnce() -> S;
}
impl<T> WelpOptionExt<T> for Option<T> {
#[inline]
#[track_caller]
fn welp_context(self, message: impl Into<String>) -> Result<T, Welp> {
match self {
Some(value) => Ok(value),
None => Err(Welp::new(message)),
}
}
#[inline]
#[track_caller]
fn with_welp_context<S, F>(self, f: F) -> Result<T, Welp>
where
S: Into<String>,
F: FnOnce() -> S,
{
match self {
Some(value) => Ok(value),
None => Err(Welp::new(f())),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{RustBacktrace, with_rust_backtrace_override};
#[cfg(feature = "tracing")]
fn with_error_subscriber<R>(f: impl FnOnce() -> R) -> R {
use tracing_subscriber::prelude::*;
let subscriber = tracing_subscriber::registry().with(tracing_error::ErrorLayer::default());
tracing::subscriber::with_default(subscriber, f)
}
#[test]
fn new_captures_message() {
let err = Welp::new("hello");
assert_eq!(err.message(), Some("hello"));
assert_eq!(err.to_string(), "hello");
}
#[test]
fn new_accepts_string_and_str() {
let _: Welp = Welp::new("static");
let _: Welp = Welp::new(String::from("owned"));
let _: Welp = Welp::new(format!("formatted {}", 1));
}
#[test]
fn new_captures_traces() {
let err = Welp::new("oops");
assert!(err.oopsie_backtrace().is_some());
assert!(err.oopsie_spantrace().is_some());
}
#[test]
fn new_has_no_source() {
let err = Welp::new("oops");
assert!(StdError::source(&err).is_none());
}
#[test]
fn captures_location_at_call_site() {
let new_line = line!() + 1;
let traced = Welp::new("solo");
let loc = traced.oopsie_location().expect("traced location");
assert!(loc.file().ends_with("welp.rs"));
assert_eq!(loc.line(), new_line);
let wrap_line = line!() + 1;
let sourced = Welp::wrap(std::io::Error::other("x"), "outer");
let loc = sourced.oopsie_location().expect("sourced location");
assert_eq!(loc.line(), wrap_line);
}
#[test]
fn wrap_carries_source() {
let inner = std::io::Error::other("disk full");
let err = Welp::wrap(inner, "could not write");
assert_eq!(err.to_string(), "could not write");
let source = StdError::source(&err).expect("source missing");
assert_eq!(source.to_string(), "disk full");
}
#[test]
fn wrap_captures_traces_at_wrap_site() {
let err = Welp::wrap(std::io::Error::other("x"), "msg");
assert!(err.oopsie_backtrace().is_some());
assert!(err.oopsie_spantrace().is_some());
}
#[test]
fn wrap_boxed_carries_source() {
let boxed: BoxError = Box::new(std::io::Error::other("disk full"));
let err = Welp::wrap_boxed(boxed, "could not write");
assert_eq!(err.to_string(), "could not write");
assert_eq!(
StdError::source(&err).expect("source missing").to_string(),
"disk full"
);
}
#[test]
fn wrap_boxed_captures_location_at_call_site() {
let boxed: BoxError = Box::new(std::io::Error::other("x"));
let wrap_line = line!() + 1;
let err = Welp::wrap_boxed(boxed, "outer");
let loc = err.oopsie_location().expect("wrap_boxed location");
assert!(loc.file().ends_with("welp.rs"));
assert_eq!(loc.line(), wrap_line);
}
#[test]
fn wrap_boxed_captures_traces_at_wrap_site() {
let boxed: BoxError = Box::new(std::io::Error::other("x"));
let err = Welp::wrap_boxed(boxed, "msg");
assert!(err.oopsie_backtrace().is_some());
assert!(err.oopsie_spantrace().is_some());
}
#[test]
fn from_error_delegates_display_to_source() {
let err = Welp::from_error(std::io::Error::other("disk full"));
assert_eq!(err.to_string(), "disk full");
assert!(err.message().is_none());
let source = StdError::source(&err).expect("source missing");
assert_eq!(source.to_string(), "disk full");
}
#[test]
fn from_error_source_downcasts_to_original() {
let err = Welp::from_error(std::io::Error::new(std::io::ErrorKind::NotFound, "missing"));
let io = StdError::source(&err)
.and_then(|s| s.downcast_ref::<std::io::Error>())
.expect("source is the original io::Error");
assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
}
#[test]
fn welp_on_ok_is_passthrough() {
let r: Result<i32, std::io::Error> = Ok(42);
assert_eq!(r.welp().unwrap(), 42);
}
#[test]
fn welp_on_err_delegates_display_and_keeps_source() {
let r: Result<i32, std::io::Error> = Err(std::io::Error::other("io fail"));
let err = r.welp().unwrap_err();
assert_eq!(err.to_string(), "io fail");
assert!(err.message().is_none());
assert_eq!(StdError::source(&err).unwrap().to_string(), "io fail");
}
#[test]
fn welp_captures_location_at_call_site() {
let call_line = line!() + 2;
let r: Result<i32, std::io::Error> = Err(std::io::Error::other("x"));
let err = r.welp().unwrap_err();
let loc = err.oopsie_location().expect("welp location");
assert!(loc.file().ends_with("welp.rs"));
assert_eq!(loc.line(), call_line);
}
#[cfg(feature = "tracing")]
#[test]
fn welp_extracts_origin_trace_from_diagnostic_source() {
let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
with_error_subscriber(|| {
let _g = tracing::info_span!("origin").entered();
let inner: Result<(), Welp> = Err(Welp::new("inner"));
inner.welp().unwrap_err()
})
});
let bt = outer.oopsie_backtrace().expect("backtrace");
assert!(!bt.frames().is_empty());
#[cfg(feature = "unstable-error-generic-member-access")]
{
let source = StdError::source(&outer)
.and_then(|s| s.downcast_ref::<Welp>())
.expect("source is the inner Welp");
let inner_bt = source.oopsie_backtrace().expect("inner backtrace");
assert!(
std::ptr::eq(
std::ptr::from_ref::<Backtrace>(bt),
std::ptr::from_ref::<Backtrace>(inner_bt)
),
"outer should surface the source's (origin-most) backtrace"
);
}
}
#[test]
fn welp_context_on_ok_is_passthrough() {
let r: Result<i32, std::io::Error> = Ok(42);
let mapped: Result<i32, Welp> = r.welp_context("nope");
assert_eq!(mapped.unwrap(), 42);
}
#[test]
fn welp_context_on_err_produces_sourced_welp() {
let r: Result<i32, std::io::Error> = Err(std::io::Error::other("io fail"));
let err = r.welp_context("read failed").unwrap_err();
assert_eq!(err.to_string(), "read failed");
assert_eq!(StdError::source(&err).unwrap().to_string(), "io fail");
}
#[test]
fn with_welp_context_does_not_run_closure_on_ok() {
let r: Result<i32, std::io::Error> = Ok(1);
let mapped: Result<i32, Welp> = r.with_welp_context(|_| -> String {
panic!("should not run");
});
mapped.unwrap();
}
#[test]
fn with_welp_context_runs_closure_on_err() {
let r: Result<i32, std::io::Error> = Err(std::io::Error::other("inner"));
let err = r
.with_welp_context(|e| format!("wrapped: {e}"))
.unwrap_err();
assert_eq!(err.to_string(), "wrapped: inner");
}
#[test]
fn option_welp_context_on_some_is_passthrough() {
let opt: Option<i32> = Some(7);
assert_eq!(opt.welp_context("missing").unwrap(), 7);
}
#[test]
fn option_welp_context_on_none_produces_traced_welp() {
let opt: Option<i32> = None;
let err = opt.welp_context("missing").unwrap_err();
assert_eq!(err.to_string(), "missing");
assert!(err.oopsie_backtrace().is_some());
assert!(StdError::source(&err).is_none());
}
#[test]
fn option_with_welp_context_does_not_run_closure_on_some() {
let opt: Option<i32> = Some(1);
let mapped: Result<i32, Welp> =
opt.with_welp_context(|| -> String { panic!("should not run") });
mapped.unwrap();
}
#[test]
fn option_with_welp_context_runs_closure_on_none() {
let opt: Option<i32> = None;
let err = opt
.with_welp_context(|| format!("missing key {}", "host"))
.unwrap_err();
assert_eq!(err.to_string(), "missing key host");
}
#[test]
fn chain_walks_welp_and_its_sources_self_first() {
use crate::ErrorChainExt as _;
let err = Welp::wrap(
Welp::wrap(std::io::Error::other("disk full"), "mid"),
"outer",
);
let messages: Vec<String> = err.chain().map(ToString::to_string).collect();
assert_eq!(messages, ["outer", "mid", "disk full"]);
assert_eq!(err.root_cause().to_string(), "disk full");
}
#[test]
fn downcast_ref_hits_matching_direct_source() {
let err = Welp::wrap(std::io::Error::other("disk full"), "outer");
let io = err.downcast_ref::<std::io::Error>().expect("source is io");
assert_eq!(io.to_string(), "disk full");
}
#[test]
fn downcast_ref_misses_on_wrong_type_and_traced() {
let sourced = Welp::wrap(std::io::Error::other("x"), "outer");
assert!(sourced.downcast_ref::<std::fmt::Error>().is_none());
let traced = Welp::new("solo");
assert!(traced.downcast_ref::<std::io::Error>().is_none());
}
#[test]
fn downcast_mut_borrows_matching_source() {
let mut err = Welp::wrap(std::io::Error::other("x"), "outer");
assert!(err.downcast_mut::<std::io::Error>().is_some());
assert!(err.downcast_mut::<std::fmt::Error>().is_none());
}
#[test]
fn downcast_recovers_matching_source() {
let err = Welp::wrap(
std::io::Error::new(std::io::ErrorKind::NotFound, "missing"),
"outer",
);
let io: std::io::Error = err.downcast().expect("source is io");
assert_eq!(io.kind(), std::io::ErrorKind::NotFound);
}
#[test]
fn downcast_only_targets_the_direct_source() {
let err = Welp::wrap(Welp::wrap(std::io::Error::other("x"), "mid"), "outer");
let err = err
.downcast::<std::io::Error>()
.expect_err("io is not the direct source");
assert!(err.downcast_ref::<Welp>().is_some());
}
#[test]
fn downcast_err_preserves_message_location_and_traces() {
let line = line!() + 1;
let err = Welp::wrap(std::io::Error::other("inner"), "outer");
let recovered = err
.downcast::<std::fmt::Error>()
.expect_err("wrong target type");
assert_eq!(recovered.message(), Some("outer"));
assert_eq!(recovered.to_string(), "outer");
let loc = recovered.oopsie_location().expect("location preserved");
assert_eq!(loc.line(), line);
assert!(recovered.oopsie_backtrace().is_some());
assert_eq!(StdError::source(&recovered).unwrap().to_string(), "inner");
}
#[test]
fn downcast_on_traced_returns_self() {
let err = Welp::new("solo");
let recovered = err
.downcast::<std::io::Error>()
.expect_err("traced has no source");
assert_eq!(recovered.message(), Some("solo"));
}
#[test]
fn debug_includes_source_for_sourced() {
let err = Welp::wrap(std::io::Error::other("inner"), "outer");
let dbg = format!("{err:?}");
assert!(dbg.contains("outer"));
assert!(dbg.contains("inner"));
}
#[test]
fn debug_omits_source_for_traced() {
let err = Welp::new("solo");
let dbg = format!("{err:?}");
assert!(dbg.contains("solo"));
assert!(!dbg.contains("source"));
}
#[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
#[test]
fn traced_provides_traces_via_request_ref() {
let err = with_rust_backtrace_override(RustBacktrace::Enabled, || {
with_error_subscriber(|| {
let _g = tracing::info_span!("solo").entered();
Welp::new("solo")
})
});
assert!(core::error::request_ref::<Backtrace>(&err).is_some());
assert!(core::error::request_ref::<SpanTrace>(&err).is_some());
}
#[cfg(feature = "unstable-error-generic-member-access")]
#[test]
fn empty_traces_are_not_provided() {
let err = with_rust_backtrace_override(RustBacktrace::Disabled, || Welp::new("solo"));
assert!(core::error::request_ref::<Backtrace>(&err).is_none());
assert!(core::error::request_ref::<SpanTrace>(&err).is_none());
}
#[cfg(feature = "tracing")]
#[test]
fn sourced_recovers_traces_from_diagnostic_source() {
let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
with_error_subscriber(|| {
let _g = tracing::info_span!("origin").entered();
Welp::wrap(Welp::new("inner"), "outer")
})
});
let bt = outer.oopsie_backtrace().expect("backtrace");
let st = outer.oopsie_spantrace().expect("spantrace");
#[cfg(feature = "unstable-error-generic-member-access")]
{
let source = StdError::source(&outer)
.and_then(|s| s.downcast_ref::<Welp>())
.expect("source is the inner Welp");
let inner_bt = source.oopsie_backtrace().expect("inner backtrace");
let inner_st = source.oopsie_spantrace().expect("inner spantrace");
assert!(
std::ptr::eq(
std::ptr::from_ref::<Backtrace>(bt),
std::ptr::from_ref::<Backtrace>(inner_bt)
),
"outer should surface the source's backtrace, not the wrap-site one"
);
assert!(
std::ptr::eq(
std::ptr::from_ref::<SpanTrace>(st),
std::ptr::from_ref::<SpanTrace>(inner_st)
),
"outer should surface the source's spantrace, not the wrap-site one"
);
}
#[cfg(not(feature = "unstable-error-generic-member-access"))]
{
let _ = (bt, st);
}
}
#[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
#[test]
fn sourced_forwards_source_provide() {
let outer = with_rust_backtrace_override(RustBacktrace::Enabled, || {
with_error_subscriber(|| {
let _g = tracing::info_span!("origin").entered();
Welp::wrap(Welp::new("inner"), "outer")
})
});
assert!(core::error::request_ref::<Backtrace>(&outer).is_some());
assert!(core::error::request_ref::<SpanTrace>(&outer).is_some());
}
#[test]
fn sourced_falls_back_to_own_traces_for_foreign_source() {
let outer = Welp::wrap(std::io::Error::other("x"), "outer");
assert!(outer.oopsie_backtrace().is_some());
assert!(outer.oopsie_spantrace().is_some());
}
#[cfg(feature = "tracing")]
#[test]
fn empty_source_spantrace_does_not_shadow_wrap_site_capture() {
let inner = Welp::new("inner"); assert!(!inner.oopsie_spantrace().unwrap().is_captured());
let outer = with_error_subscriber(|| {
let _g = tracing::info_span!("wrap_site").entered();
Welp::wrap(inner, "outer")
});
assert!(outer.oopsie_spantrace().unwrap().is_captured());
}
#[test]
fn empty_source_backtrace_does_not_shadow_wrap_site_capture() {
let inner = with_rust_backtrace_override(RustBacktrace::Disabled, || Welp::new("inner"));
let outer =
with_rust_backtrace_override(RustBacktrace::Enabled, || Welp::wrap(inner, "outer"));
assert!(!outer.oopsie_backtrace().unwrap().frames().is_empty());
}
#[cfg(all(feature = "unstable-error-generic-member-access", feature = "tracing"))]
#[test]
fn wrap_surfaces_captured_source_spantrace() {
let inner = with_error_subscriber(|| {
let _g = tracing::info_span!("origin").entered();
Welp::new("inner")
});
assert!(inner.oopsie_spantrace().unwrap().is_captured());
let outer = Welp::wrap(inner, "outer"); assert!(outer.oopsie_spantrace().unwrap().is_captured());
}
const _: () = {
const fn assert_send<T: Send>() {}
const fn assert_sync<T: Sync>() {}
const fn assert_static<T: 'static>() {}
assert_send::<Welp>();
assert_sync::<Welp>();
assert_static::<Welp>();
};
#[test]
fn welp_size_is_pinned() {
assert_eq!(std::mem::size_of::<Welp>(), 48);
}
}