use core::error;
use std::{panic::Location, rc::Rc, sync::Arc};
#[diagnostic::on_unimplemented(
message = "`{Self}` cannot build an error from a source of type `{E}`",
label = "this selector does not accept a `{E}` source",
note = "a selector whose variant has a `source` field builds from that field's exact type — \
`.context(...)` on a `Result` requires the selector's source type to match the \
`Result`'s error",
note = "a selector with no `source` field is a leaf: build it with `.build()` / `.fail()`, or \
attach it to an `Option` with `.context(...)` — not to a `Result`'s error"
)]
pub trait Contextual<E> {
type Destination;
#[track_caller]
fn build_error(self, source: E) -> Self::Destination;
}
pub trait Capturable {
#[track_caller]
fn capture() -> Self;
#[track_caller]
#[inline]
fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self
where
Self: Sized,
{
let _ = source;
Self::capture()
}
}
impl<T: Capturable> Capturable for Box<T> {
#[track_caller]
#[inline]
fn capture() -> Self {
Self::new(T::capture())
}
#[track_caller]
#[inline]
fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
Self::new(T::capture_or_extract(source))
}
}
impl<T: Capturable> Capturable for Rc<T> {
#[track_caller]
#[inline]
fn capture() -> Self {
Self::new(T::capture())
}
#[track_caller]
#[inline]
fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
Self::new(T::capture_or_extract(source))
}
}
impl<T: Capturable> Capturable for Arc<T> {
#[track_caller]
#[inline]
fn capture() -> Self {
Self::new(T::capture())
}
#[track_caller]
#[inline]
fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
Self::new(T::capture_or_extract(source))
}
}
macro_rules! impl_capturable_tuples {
($($name:ident),* $(,)?) => {
impl_capturable_tuples!(@acc [] $($name,)*);
};
(@acc [$($acc:ident,)*] $head:ident, $($rest:ident,)*) => {
impl_capturable_tuples!(@impl $($acc,)* $head,);
impl_capturable_tuples!(@acc [$($acc,)* $head,] $($rest,)*);
};
(@acc [$($acc:ident,)*]) => {};
(@impl $($name:ident,)+) => {
impl<$($name: Capturable),+> Capturable for ($($name,)+) {
#[track_caller]
#[inline]
fn capture() -> Self {
($($name::capture(),)+)
}
#[track_caller]
#[inline]
fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
($($name::capture_or_extract(source),)+)
}
}
};
}
impl_capturable_tuples!(A, B, C, D, E, F, G, H, I);
impl Capturable for std::time::SystemTime {
#[inline]
fn capture() -> Self {
Self::now()
}
}
impl Capturable for std::time::Instant {
#[inline]
fn capture() -> Self {
Self::now()
}
}
impl Capturable for &'static Location<'static> {
#[inline]
#[track_caller]
fn capture() -> Self {
Location::caller()
}
#[track_caller]
#[inline]
fn capture_or_extract(source: &dyn crate::Diagnostic) -> Self {
let here = Location::caller();
source.oopsie_location().unwrap_or(here)
}
}
#[cfg(feature = "chrono")]
impl Capturable for chrono::DateTime<chrono::Local> {
#[inline]
fn capture() -> Self {
chrono::Local::now()
}
}
#[cfg(feature = "jiff")]
impl Capturable for jiff::Timestamp {
#[inline]
fn capture() -> Self {
Self::now()
}
}
#[cfg(feature = "jiff")]
impl Capturable for jiff::Zoned {
#[inline]
fn capture() -> Self {
Self::now()
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub struct NoSource;
pub trait AsErrorSource {
fn as_error_source(&self) -> &(dyn error::Error + 'static);
}
impl<T: error::Error + 'static> AsErrorSource for T {
#[inline]
fn as_error_source(&self) -> &(dyn error::Error + 'static) {
self
}
}
impl AsErrorSource for dyn error::Error + 'static {
#[inline]
fn as_error_source(&self) -> &(dyn error::Error + 'static) {
self
}
}
impl AsErrorSource for dyn error::Error + Send + 'static {
#[inline]
fn as_error_source(&self) -> &(dyn error::Error + 'static) {
self
}
}
impl AsErrorSource for dyn error::Error + Send + Sync + 'static {
#[inline]
fn as_error_source(&self) -> &(dyn error::Error + 'static) {
self
}
}
pub trait ResultExt<T, E> {
fn context<C>(self, context: C) -> Result<T, C::Destination>
where
C: Contextual<E>;
fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
where
F: FnOnce(&E) -> C,
C: Contextual<E>;
fn boxed(self) -> Result<T, Box<dyn error::Error + Send + Sync + 'static>>
where
E: error::Error + Send + Sync + 'static;
fn boxed_local(self) -> Result<T, Box<dyn error::Error + 'static>>
where
E: error::Error + 'static;
}
impl<T, E> ResultExt<T, E> for Result<T, E> {
#[inline]
#[track_caller]
fn context<C>(self, context: C) -> Result<T, C::Destination>
where
C: Contextual<E>,
{
match self {
Ok(value) => Ok(value),
Err(error) => Err(context.build_error(error)),
}
}
#[inline]
#[track_caller]
fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
where
F: FnOnce(&E) -> C,
C: Contextual<E>,
{
match self {
Ok(value) => Ok(value),
Err(error) => {
let selector = context(&error);
Err(selector.build_error(error))
}
}
}
#[inline]
fn boxed(self) -> Result<T, Box<dyn error::Error + Send + Sync + 'static>>
where
E: error::Error + Send + Sync + 'static,
{
self.map_err(|error| Box::new(error) as Box<dyn error::Error + Send + Sync + 'static>)
}
#[inline]
fn boxed_local(self) -> Result<T, Box<dyn error::Error + 'static>>
where
E: error::Error + 'static,
{
self.map_err(|error| Box::new(error) as Box<dyn error::Error + 'static>)
}
}
pub trait OptionExt<T> {
fn context<C>(self, context: C) -> Result<T, C::Destination>
where
C: Contextual<NoSource>;
fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
where
F: FnOnce() -> C,
C: Contextual<NoSource>;
}
impl<T> OptionExt<T> for Option<T> {
#[inline]
#[track_caller]
fn context<C>(self, context: C) -> Result<T, C::Destination>
where
C: Contextual<NoSource>,
{
match self {
Some(value) => Ok(value),
None => Err(context.build_error(NoSource)),
}
}
#[inline]
#[track_caller]
fn with_context<F, C>(self, context: F) -> Result<T, C::Destination>
where
F: FnOnce() -> C,
C: Contextual<NoSource>,
{
match self {
Some(value) => Ok(value),
None => Err(context().build_error(NoSource)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
use std::fmt;
use std::time::SystemTime;
#[cfg(feature = "jiff")]
#[test]
fn captures_current_jiff_timestamp() {
let before = jiff::Timestamp::now();
let captured = <jiff::Timestamp as Capturable>::capture();
assert!(captured >= before, "captured {captured} predates {before}");
}
#[cfg(feature = "jiff")]
#[test]
fn captures_current_jiff_zoned() {
let before = jiff::Timestamp::now();
let captured = <jiff::Zoned as Capturable>::capture().timestamp();
assert!(captured >= before, "captured {captured} predates {before}");
}
#[derive(Debug)]
struct DiagOnly;
impl fmt::Display for DiagOnly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("diag only")
}
}
impl std::error::Error for DiagOnly {}
impl crate::Diagnostic for DiagOnly {}
#[derive(Debug)]
struct SimpleError {
message: String,
}
impl fmt::Display for SimpleError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for SimpleError {}
#[derive(Debug)]
struct SourceError {
message: String,
}
impl fmt::Display for SourceError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for SourceError {}
#[derive(Debug)]
struct ChainError {
message: String,
source: Box<SourceError>,
}
impl fmt::Display for ChainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl std::error::Error for ChainError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.source)
}
}
struct SimpleSelector;
impl Contextual<NoSource> for SimpleSelector {
type Destination = SimpleError;
fn build_error(self, _source: NoSource) -> SimpleError {
SimpleError {
message: "simple error".to_owned(),
}
}
}
struct ChainSelector;
impl Contextual<SourceError> for ChainSelector {
type Destination = ChainError;
fn build_error(self, source: SourceError) -> ChainError {
ChainError {
message: "chain error".to_owned(),
source: Box::new(source),
}
}
}
#[test]
fn test_result_ext_context_ok() {
let result: Result<i32, SourceError> = Ok(42);
let chained: Result<i32, ChainError> = result.context(ChainSelector);
assert!(chained.is_ok());
assert_eq!(chained.unwrap(), 42);
}
#[test]
fn test_result_ext_context_err() {
let result: Result<i32, SourceError> = Err(SourceError {
message: "source failed".to_owned(),
});
let chained: Result<i32, ChainError> = result.context(ChainSelector);
assert!(chained.is_err());
let err = chained.unwrap_err();
assert_eq!(err.message, "chain error");
assert_eq!(StdError::source(&err).unwrap().to_string(), "source failed");
}
#[test]
fn test_result_ext_with_context_ok() {
let result: Result<i32, SourceError> = Ok(42);
let chained: Result<i32, ChainError> = result.with_context(|_| ChainSelector);
assert!(chained.is_ok());
assert_eq!(chained.unwrap(), 42);
}
#[test]
fn test_result_ext_with_context_err_closure_called() {
let mut closure_called = false;
let result: Result<i32, SourceError> = Err(SourceError {
message: "source failed".to_owned(),
});
let chained: Result<i32, ChainError> = result.with_context(|source| {
closure_called = true;
assert_eq!(source.message, "source failed");
ChainSelector
});
assert!(closure_called);
chained.unwrap_err();
}
#[test]
fn test_option_ext_context_some() {
let option: Option<i32> = Some(42);
let result: Result<i32, SimpleError> = option.context(SimpleSelector);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_option_ext_context_none() {
let option: Option<i32> = None;
let result: Result<i32, SimpleError> = option.context(SimpleSelector);
assert!(result.is_err());
assert_eq!(result.unwrap_err().message, "simple error");
}
#[test]
fn test_option_ext_with_context_some() {
let option: Option<i32> = Some(42);
let result: Result<i32, SimpleError> = option.with_context(|| SimpleSelector);
assert!(result.is_ok());
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_option_ext_with_context_none_closure_called() {
let mut closure_called = false;
let option: Option<i32> = None;
let result: Result<i32, SimpleError> = option.with_context(|| {
closure_called = true;
SimpleSelector
});
assert!(closure_called);
result.unwrap_err();
}
const _: () = {
const fn is_capturable<T: Capturable>() {}
is_capturable::<Box<crate::Backtrace>>();
};
#[cfg(feature = "tracing")]
#[test]
fn tuple_capture_produces_both_elements() {
use crate::{Backtrace, SpanTrace};
let (bt, st): (Backtrace, SpanTrace) = <(Backtrace, SpanTrace) as Capturable>::capture();
let _ = bt.frames();
let _ = st.status();
}
#[cfg(feature = "tracing")]
const _: () = {
const fn is_capturable<T: Capturable>() {}
is_capturable::<Box<(crate::Backtrace, crate::SpanTrace)>>();
is_capturable::<(crate::Backtrace, crate::SpanTrace)>();
};
#[cfg(feature = "tracing")]
#[test]
fn tuple_capture_ext_extracts_both_from_source() {
use crate::{
Backtrace, Capturable, Diagnostic, RustBacktrace, SpanTrace,
with_rust_backtrace_override,
};
use std::fmt;
#[derive(Debug)]
struct Src {
backtrace: Backtrace,
spantrace: SpanTrace,
}
impl fmt::Display for Src {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("src")
}
}
impl std::error::Error for Src {}
impl Diagnostic for Src {
fn oopsie_backtrace(&self) -> Option<&Backtrace> {
Some(&self.backtrace)
}
fn oopsie_spantrace(&self) -> Option<&SpanTrace> {
Some(&self.spantrace)
}
}
with_rust_backtrace_override(RustBacktrace::Enabled, || {
let src = Src {
backtrace: Backtrace::capture(),
spantrace: SpanTrace::capture(),
};
let source_frames = src.backtrace.frames().len();
assert!(
source_frames > 0,
"backtrace must be enabled for this test to be probative"
);
let extracted = <(Backtrace, SpanTrace) as Capturable>::capture_or_extract(&src);
assert_eq!(extracted.0.frames().len(), source_frames);
});
}
#[cfg(feature = "tracing")]
const _: () = {
const fn is_capturable<T: Capturable>() {}
is_capturable::<Box<(crate::Backtrace, crate::SpanTrace)>>();
is_capturable::<(crate::Backtrace, crate::SpanTrace)>();
};
#[derive(Debug)]
struct BoxedChainError {
message: String,
source: Box<dyn StdError + Send + Sync + 'static>,
}
impl fmt::Display for BoxedChainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)
}
}
impl StdError for BoxedChainError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&*self.source)
}
}
struct BoxedSelector;
impl Contextual<Box<dyn StdError + Send + Sync + 'static>> for BoxedSelector {
type Destination = BoxedChainError;
fn build_error(self, source: Box<dyn StdError + Send + Sync + 'static>) -> BoxedChainError {
BoxedChainError {
message: "boxed chain error".to_owned(),
source,
}
}
}
#[derive(Debug)]
struct BoxedLocalChainError {
source: Box<dyn StdError + 'static>,
}
impl fmt::Display for BoxedLocalChainError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "boxed local chain error")
}
}
impl StdError for BoxedLocalChainError {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
Some(&*self.source)
}
}
struct BoxedLocalSelector;
impl Contextual<Box<dyn StdError + 'static>> for BoxedLocalSelector {
type Destination = BoxedLocalChainError;
fn build_error(self, source: Box<dyn StdError + 'static>) -> BoxedLocalChainError {
BoxedLocalChainError { source }
}
}
#[test]
fn boxed_passes_through_ok() {
let result: Result<i32, SourceError> = Ok(7);
assert_eq!(result.boxed().unwrap(), 7);
}
#[test]
fn boxed_erases_err_but_preserves_display_and_concrete_type() {
let result: Result<i32, SourceError> = Err(SourceError {
message: "boom".to_owned(),
});
let err = result.boxed().unwrap_err();
assert_eq!(err.to_string(), "boom");
assert!(err.downcast_ref::<SourceError>().is_some());
}
#[test]
fn boxed_local_erases_err_but_preserves_display_and_concrete_type() {
let result: Result<i32, SourceError> = Err(SourceError {
message: "boom".to_owned(),
});
let err = result.boxed_local().unwrap_err();
assert_eq!(err.to_string(), "boom");
assert!(err.downcast_ref::<SourceError>().is_some());
}
#[test]
fn boxed_then_context_chains_erased_source() {
let result: Result<i32, SourceError> = Err(SourceError {
message: "io broke".to_owned(),
});
let chained: Result<i32, BoxedChainError> = result.boxed().context(BoxedSelector);
let err = chained.unwrap_err();
assert_eq!(err.message, "boxed chain error");
assert_eq!(StdError::source(&err).unwrap().to_string(), "io broke");
}
#[test]
fn boxed_then_context_passes_through_ok() {
let result: Result<i32, SourceError> = Ok(42);
let chained: Result<i32, BoxedChainError> = result.boxed().context(BoxedSelector);
assert_eq!(chained.unwrap(), 42);
}
#[test]
fn boxed_then_with_context_chains_erased_source() {
let result: Result<i32, SourceError> = Err(SourceError {
message: "io broke".to_owned(),
});
let chained: Result<i32, BoxedChainError> = result.boxed().with_context(|source| {
assert_eq!(source.to_string(), "io broke");
BoxedSelector
});
assert_eq!(
StdError::source(&chained.unwrap_err()).unwrap().to_string(),
"io broke"
);
}
#[test]
fn boxed_local_then_context_chains_erased_source() {
let result: Result<i32, SourceError> = Err(SourceError {
message: "io broke".to_owned(),
});
let chained: Result<i32, BoxedLocalChainError> =
result.boxed_local().context(BoxedLocalSelector);
assert_eq!(
StdError::source(&chained.unwrap_err()).unwrap().to_string(),
"io broke"
);
}
#[test]
fn boxed_unifies_heterogeneous_error_types() {
fn run(fail_with_source: bool) -> Result<(), Box<dyn StdError + Send + Sync + 'static>> {
if fail_with_source {
Err(SourceError {
message: "source".to_owned(),
})
.boxed()?;
} else {
Err(SimpleError {
message: "simple".to_owned(),
})
.boxed()?;
}
Ok(())
}
assert_eq!(run(true).unwrap_err().to_string(), "source");
assert_eq!(run(false).unwrap_err().to_string(), "simple");
}
#[test]
fn boxed_on_boxed_error_downcasts_to_the_box() {
let result: Result<(), Box<SourceError>> = Err(Box::new(SourceError {
message: "boom".to_owned(),
}));
let err = result.boxed().unwrap_err();
assert!(err.downcast_ref::<SourceError>().is_none());
assert!(err.downcast_ref::<Box<SourceError>>().is_some());
}
const _: () = {
const fn is_send_sync<T: Send + Sync>() {}
is_send_sync::<Box<dyn StdError + Send + Sync + 'static>>();
};
#[derive(Debug)]
struct WithLoc(&'static std::panic::Location<'static>);
impl fmt::Display for WithLoc {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("with loc")
}
}
impl StdError for WithLoc {}
impl crate::Diagnostic for WithLoc {
fn oopsie_location(&self) -> Option<&'static std::panic::Location<'static>> {
Some(self.0)
}
}
#[test]
fn location_captures_caller_and_extracts_from_source() {
let line = line!() + 1;
let loc = <&'static std::panic::Location<'static> as Capturable>::capture();
assert!(loc.file().ends_with("traits.rs"));
assert_eq!(loc.line(), line);
let src = WithLoc(std::panic::Location::caller());
let extracted =
<&'static std::panic::Location<'static> as Capturable>::capture_or_extract(&src);
assert!(
std::ptr::eq(extracted, src.0),
"must reuse the source's location"
);
let fresh =
<&'static std::panic::Location<'static> as Capturable>::capture_or_extract(&DiagOnly);
assert!(fresh.file().ends_with("traits.rs"));
}
#[test]
fn system_time_captures_now_and_never_extracts() {
let before = SystemTime::now();
let captured = <SystemTime as Capturable>::capture();
assert!(captured >= before);
let extracted = <SystemTime as Capturable>::capture_or_extract(&DiagOnly);
assert!(extracted >= before);
}
#[test]
fn location_captures() {
let location = <&'static Location<'static> as Capturable>::capture();
let expected_line = line!() - 1; assert_eq!(location.line(), expected_line);
assert_eq!(location.file(), file!());
}
#[test]
fn location_captures_nested() {
#[track_caller]
fn capture_location() -> &'static Location<'static> {
<&'static Location<'static> as Capturable>::capture()
}
let location = capture_location();
let expected_line = line!() - 1; assert_eq!(location.line(), expected_line);
assert_eq!(location.file(), file!());
}
#[test]
fn complex_location_captures() {
type Loc = &'static Location<'static>;
type LocTimeLoc = (Loc, SystemTime, Loc);
let location =
<(Box<LocTimeLoc>, Rc<LocTimeLoc>, Arc<LocTimeLoc>) as Capturable>::capture();
let expected_line = line!() - 1; let expected_col = 3 * 4 + 1; let (box_loc, rc_loc, arc_loc) = location;
for (loc1, _, loc2) in [&*box_loc, &*rc_loc, &*arc_loc] {
for loc in [loc1, loc2] {
assert_eq!(loc.file(), file!());
assert_eq!(loc.line(), expected_line);
assert_eq!(loc.column(), expected_col);
}
}
}
}