use std::error::Error;
use crate::fail::Fault;
use crate::lane::{Denied, Exhausted, Fatal, FatalKind, Lane, Transient};
impl Lane {
pub fn of(e: &(dyn Error + 'static)) -> Option<Lane> {
let mut cur: Option<&(dyn Error + 'static)> = Some(e);
while let Some(x) = cur {
if x.is::<Transient>() {
return Some(Lane::Transient);
}
if x.is::<Fatal>() || x.is::<Exhausted>() {
return Some(Lane::Fatal);
}
if x.is::<Denied>() {
return Some(Lane::Denied);
}
cur = x.source();
}
None
}
}
fn find_in(e: &(dyn Error + 'static)) -> Option<Fault> {
let mut cur: Option<&(dyn Error + 'static)> = Some(e);
while let Some(x) = cur {
if let Some(t) = x.downcast_ref::<Transient>() {
return Some(Fault::Transient(t.clone()));
}
if let Some(f) = x.downcast_ref::<Fatal>() {
return Some(Fault::Fatal(f.clone()));
}
if let Some(ex) = x.downcast_ref::<Exhausted>() {
return Some(Fault::Fatal(Fatal::from_error(
FatalKind::Exhausted,
ex.clone(),
)));
}
if let Some(d) = x.downcast_ref::<Denied>() {
return Some(Fault::Denied(d.clone()));
}
cur = x.source();
}
None
}
pub(crate) fn message_chain(e: &(dyn Error + 'static)) -> String {
let mut parts = Vec::new();
let mut cur: Option<&(dyn Error + 'static)> = Some(e);
while let Some(x) = cur {
parts.push(x.to_string());
if x.downcast_ref::<Fatal>()
.is_some_and(Fatal::has_opaque_source)
{
break;
}
cur = x.source();
}
parts.join(": ")
}
impl Fault {
pub fn classify(e: &(dyn Error + 'static)) -> Fault {
if let Some(f) = find_in(e) {
return f;
}
#[cfg(feature = "classify-sqlx")]
{
let mut cur: Option<&(dyn Error + 'static)> = Some(e);
while let Some(x) = cur {
if let Some(sql) = x.downcast_ref::<::sqlx::Error>() {
return crate::sqlx::classify_sqlx_ref(sql).into();
}
cur = x.source();
}
}
#[cfg(feature = "classify-serde-json")]
{
let mut cur: Option<&(dyn Error + 'static)> = Some(e);
while let Some(x) = cur {
if let Some(json) = x.downcast_ref::<::serde_json::Error>() {
return crate::serde_json::classify_serde_json_ref(json).into();
}
cur = x.source();
}
}
#[cfg(feature = "classify-reqwest")]
{
let mut cur: Option<&(dyn Error + 'static)> = Some(e);
while let Some(x) = cur {
if let Some(req) = x.downcast_ref::<::reqwest::Error>() {
return crate::reqwest::classify_reqwest_ref(req);
}
cur = x.source();
}
}
Fault::Fatal(Fatal::new(FatalKind::Dependency).with_context(message_chain(e)))
}
}
#[cfg(test)]
mod tests {
use std::fmt;
use super::*;
use crate::lane::{FatalKind, TransientKind};
#[derive(Debug)]
struct Wrapped<E>(E);
impl<E: fmt::Display> fmt::Display for Wrapped<E> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "wrapped: {}", self.0)
}
}
impl<E: Error + 'static> Error for Wrapped<E> {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.0)
}
}
#[test]
fn finds_transient_three_levels_deep() {
let t = Transient::new(TransientKind::Deadlock);
let chain = Wrapped(Wrapped(Wrapped(t)));
assert_eq!(Lane::of(&chain), Some(Lane::Transient));
assert!(matches!(find_in(&chain), Some(Fault::Transient(_))));
}
#[test]
fn finds_fatal_three_levels_deep() {
let f = Fatal::new(FatalKind::CorruptState);
let chain = Wrapped(Wrapped(Wrapped(f)));
assert_eq!(Lane::of(&chain), Some(Lane::Fatal));
assert!(matches!(find_in(&chain), Some(Fault::Fatal(_))));
}
#[test]
fn finds_denied_three_levels_deep() {
let d = Denied::default();
let chain = Wrapped(Wrapped(Wrapped(d)));
assert_eq!(Lane::of(&chain), Some(Lane::Denied));
assert!(matches!(find_in(&chain), Some(Fault::Denied(_))));
}
#[test]
fn find_in_exhausted_arrives_as_the_wrapping_fatal() {
use crate::lane::Exhausted;
let fatal = Fatal::from_error(
FatalKind::Exhausted,
Exhausted {
attempts: 3,
last: Transient::new(TransientKind::Deadlock),
},
);
let chain = Wrapped(fatal);
assert_eq!(Lane::of(&chain), Some(Lane::Fatal));
match find_in(&chain) {
Some(Fault::Fatal(f)) => {
assert_eq!(f.kind, FatalKind::Exhausted);
assert!(f.source().unwrap().downcast_ref::<Exhausted>().is_some());
}
other => panic!("expected Fault::Fatal wrapping Exhausted, got {other:?}"),
}
}
#[test]
fn bare_exhausted_classifies_as_fatal_not_its_inner_transient() {
use crate::lane::Exhausted;
let exhausted = Exhausted {
attempts: 3,
last: Transient::new(TransientKind::Deadlock),
};
assert_eq!(Lane::of(&exhausted), Some(Lane::Fatal));
match find_in(&exhausted) {
Some(Fault::Fatal(f)) => assert_eq!(f.kind, FatalKind::Exhausted),
other => panic!("expected Fault::Fatal(Exhausted), got {other:?}"),
}
let chain = Wrapped(exhausted);
assert_eq!(Lane::of(&chain), Some(Lane::Fatal));
match find_in(&chain) {
Some(Fault::Fatal(f)) => assert_eq!(f.kind, FatalKind::Exhausted),
other => panic!("expected Fault::Fatal(Exhausted), got {other:?}"),
}
}
#[test]
fn message_chain_joins_display_of_every_hop() {
#[derive(Debug)]
struct Inner;
impl fmt::Display for Inner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "inner")
}
}
impl Error for Inner {}
#[derive(Debug)]
struct Outer(Inner);
impl fmt::Display for Outer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "outer")
}
}
impl Error for Outer {
fn source(&self) -> Option<&(dyn Error + 'static)> {
Some(&self.0)
}
}
assert_eq!(message_chain(&Outer(Inner)), "outer: inner");
}
#[test]
fn classify_finds_a_laned_error_in_the_chain() {
let t = Transient::new(TransientKind::Deadlock);
let chain = Wrapped(Wrapped(t));
match Fault::classify(&chain) {
Fault::Transient(t) => assert_eq!(t.kind, TransientKind::Deadlock),
other => panic!("expected Fault::Transient, got {other:?}"),
}
}
#[test]
fn classify_falls_back_to_fatal_dependency() {
let io = std::io::Error::other("disk full");
match Fault::classify(&io) {
Fault::Fatal(f) => {
assert_eq!(f.kind, FatalKind::Dependency);
assert_eq!(f.context.as_deref(), Some("disk full"));
}
other => panic!("expected Fault::Fatal(Dependency), got {other:?}"),
}
}
#[test]
fn classify_result_is_send_even_when_the_source_error_is_not() {
use std::rc::Rc;
#[derive(Debug)]
struct NotSend(#[allow(dead_code)] Rc<()>);
impl fmt::Display for NotSend {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "not send")
}
}
impl Error for NotSend {}
let boxed: Box<dyn Error> = Box::new(NotSend(Rc::new(())));
let fault = Fault::classify(&*boxed);
drop(boxed); let handle = std::thread::spawn(move || {
assert!(matches!(fault, Fault::Fatal(_)));
});
handle.join().unwrap();
}
#[test]
fn bare_non_lanes_error_yields_none() {
#[derive(Debug)]
struct Plain;
impl std::fmt::Display for Plain {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "plain")
}
}
impl Error for Plain {}
assert_eq!(Lane::of(&Plain), None);
assert!(find_in(&Plain).is_none());
}
}