#![forbid(unsafe_code)]
#![deny(missing_docs)]
use std::{
cell::RefCell,
error,
fmt::{self, Display},
future::Future,
ops,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
#[derive(Debug, Clone)]
#[repr(transparent)]
pub struct Error(Arc<dyn error::Error + Send + Sync>);
impl Error {
pub fn new<E>(err: E) -> Self
where
E: error::Error + Send + Sync + 'static,
{
Error(Arc::new(err))
}
pub fn into_inner(self) -> Arc<dyn error::Error + Send + Sync> {
self.0
}
}
impl ops::Deref for Error {
type Target = Arc<dyn error::Error + Send + Sync>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.0)
}
}
impl<T> From<T> for Error
where
T: Into<Box<dyn error::Error + Send + Sync + 'static>>,
{
fn from(value: T) -> Self {
Error(Arc::from(value.into()))
}
}
pub trait ErrorHook: Send + Sync {
fn throw(&self, error: Error) -> ErrorId;
fn clear(&self, id: &ErrorId);
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Default)]
pub struct ErrorId(usize);
impl Display for ErrorId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
Display::fmt(&self.0, f)
}
}
impl From<usize> for ErrorId {
fn from(value: usize) -> Self {
Self(value)
}
}
thread_local! {
static ERROR_HOOK: RefCell<Option<Arc<dyn ErrorHook>>> = RefCell::new(None);
}
pub struct ResetErrorHookOnDrop(Option<Arc<dyn ErrorHook>>);
impl Drop for ResetErrorHookOnDrop {
fn drop(&mut self) {
let _ = ERROR_HOOK.try_with(|cell| {
if let Ok(mut slot) = cell.try_borrow_mut() {
*slot = self.0.take();
}
});
}
}
pub fn get_error_hook() -> Option<Arc<dyn ErrorHook>> {
ERROR_HOOK.with_borrow(Clone::clone)
}
pub fn set_error_hook(hook: Arc<dyn ErrorHook>) -> ResetErrorHookOnDrop {
ResetErrorHookOnDrop(
ERROR_HOOK.with_borrow_mut(|this| Option::replace(this, hook)),
)
}
pub fn throw(error: impl Into<Error>) -> ErrorId {
get_error_hook()
.map(|hook| hook.throw(error.into()))
.unwrap_or_default()
}
pub fn clear(id: &ErrorId) {
if let Some(hook) = get_error_hook() {
hook.clear(id);
}
}
pin_project_lite::pin_project! {
pub struct ErrorHookFuture<Fut> {
hook: Option<Arc<dyn ErrorHook>>,
#[pin]
inner: Fut
}
}
impl<Fut> ErrorHookFuture<Fut> {
pub fn new(inner: Fut) -> Self {
Self {
hook: ERROR_HOOK.with_borrow(Clone::clone),
inner,
}
}
}
impl<Fut> Future for ErrorHookFuture<Fut>
where
Fut: Future,
{
type Output = Fut::Output;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
let _hook =
ResetErrorHookOnDrop(ERROR_HOOK.with_borrow_mut(|cur| {
std::mem::replace(cur, this.hook.clone())
}));
this.inner.poll(cx)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error as StdError;
#[derive(Debug)]
struct MyError;
impl Display for MyError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "MyError")
}
}
impl StdError for MyError {}
#[test]
fn test_from() {
let e = MyError;
let _le = Error::from(e);
let e = "some error".to_string();
let _le = Error::from(e);
let e = anyhow::anyhow!("anyhow error");
let _le = Error::from(e);
}
#[test]
fn new_wraps_concrete_error_in_single_arc() {
let e = Error::new(MyError);
assert_eq!(e.to_string(), "MyError");
assert_eq!(e.to_string(), Error::from(MyError).to_string());
let inner = e.into_inner();
assert_eq!(Arc::strong_count(&inner), 1);
}
#[test]
fn into_inner_yields_uniquely_owned_arc() {
let e: Error = "boom".to_string().into();
let inner = e.into_inner();
assert_eq!(Arc::strong_count(&inner), 1);
let e: Error = "boom".to_string().into();
let clone = e.clone();
let inner = e.into_inner();
assert_eq!(Arc::strong_count(&inner), 2);
drop(clone);
assert_eq!(Arc::strong_count(&inner), 1);
}
struct NoOpHook;
impl ErrorHook for NoOpHook {
fn throw(&self, _: Error) -> ErrorId {
ErrorId::default()
}
fn clear(&self, _: &ErrorId) {}
}
#[test]
fn hook_may_reenter_error_hook_machinery_from_callback() {
struct Reenter;
impl ErrorHook for Reenter {
fn throw(&self, _: Error) -> ErrorId {
let _guard = set_error_hook(Arc::new(NoOpHook));
ErrorId::default()
}
fn clear(&self, _id: &ErrorId) {
let _guard = set_error_hook(Arc::new(NoOpHook));
}
}
let _guard = set_error_hook(Arc::new(Reenter));
let id = throw("boom");
clear(&id);
}
#[test]
fn error_hook_future_with_no_captured_hook_clears_ambient_hook_during_poll()
{
use std::sync::atomic::{AtomicUsize, Ordering};
struct Counter(AtomicUsize);
impl ErrorHook for Counter {
fn throw(&self, _: Error) -> ErrorId {
self.0.fetch_add(1, Ordering::SeqCst);
ErrorId::default()
}
fn clear(&self, _: &ErrorId) {}
}
let fut = ErrorHookFuture::new(async {
throw("inside future");
});
let counter = Arc::new(Counter(AtomicUsize::new(0)));
let _g = set_error_hook(counter.clone());
let mut fut = std::pin::pin!(fut);
let mut cx = Context::from_waker(std::task::Waker::noop());
assert!(fut.as_mut().poll(&mut cx).is_ready());
assert_eq!(counter.0.load(Ordering::SeqCst), 0);
assert!(get_error_hook().is_some());
}
#[test]
fn reset_guard_drop_is_silent_while_hook_slot_is_borrowed() {
let guard = set_error_hook(Arc::new(NoOpHook));
ERROR_HOOK.with_borrow(|_held| {
drop(guard);
});
}
}