use thiserror::Error;
type BoxedError = Box<dyn std::error::Error + Send + Sync>;
#[derive(Debug, Error)]
#[non_exhaustive]
pub enum Error {
#[error("retry '{name}' exhausted after {attempts} attempts: {source}")]
Exhausted {
name: String,
attempts: u32,
#[source]
source: BoxedError,
},
#[error("retry '{name}' encountered permanent error: {source}")]
Permanent {
name: String,
#[source]
source: BoxedError,
},
#[error("retry '{name}' cancelled")]
Cancelled {
name: String,
},
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug)]
pub struct PermanentError {
pub(crate) inner: BoxedError,
}
impl PermanentError {
#[must_use]
pub fn wrap<E>(e: E) -> Self
where
E: Into<BoxedError>,
{
Self { inner: e.into() }
}
}
impl std::fmt::Display for PermanentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.inner.fmt(f)
}
}
impl std::error::Error for PermanentError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
Some(&*self.inner)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn exhausted_includes_name_and_count() {
let e = Error::Exhausted {
name: "db.connect".into(),
attempts: 3,
source: "ENETUNREACH".into(),
};
assert!(e.to_string().contains("db.connect"));
assert!(e.to_string().contains("3 attempts"));
}
#[test]
fn permanent_wrap_preserves_message() {
let p = PermanentError::wrap("invalid token");
assert_eq!(p.to_string(), "invalid token");
}
}