use std::error::Error;
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, serde::Serialize, serde::Deserialize)]
pub enum TransportErrorKind {
Retryable,
Permanent,
}
impl TransportErrorKind {
pub fn is_retryable(self) -> bool {
matches!(self, TransportErrorKind::Retryable)
}
pub fn is_permanent(self) -> bool {
matches!(self, TransportErrorKind::Permanent)
}
}
#[derive(Debug)]
pub struct TransportError {
kind: TransportErrorKind,
message: String,
source: Option<Box<dyn Error + Send + Sync>>,
retain_and_stop: bool,
}
impl TransportError {
pub fn retryable(message: impl Into<String>) -> Self {
Self {
kind: TransportErrorKind::Retryable,
message: message.into(),
source: None,
retain_and_stop: false,
}
}
pub fn permanent(message: impl Into<String>) -> Self {
Self {
kind: TransportErrorKind::Permanent,
message: message.into(),
source: None,
retain_and_stop: false,
}
}
pub fn new(kind: TransportErrorKind, message: impl Into<String>) -> Self {
Self {
kind,
message: message.into(),
source: None,
retain_and_stop: false,
}
}
pub fn with_source(mut self, source: impl Error + Send + Sync + 'static) -> Self {
self.source = Some(Box::new(source));
self
}
pub(crate) fn retain_and_stop(mut self) -> Self {
self.retain_and_stop = true;
self
}
pub(crate) fn should_retain_and_stop(&self) -> bool {
self.retain_and_stop
}
pub fn kind(&self) -> TransportErrorKind {
self.kind
}
pub fn is_retryable(&self) -> bool {
self.kind.is_retryable()
}
pub fn is_permanent(&self) -> bool {
self.kind.is_permanent()
}
pub fn message(&self) -> &str {
&self.message
}
}
#[cfg(any(feature = "nats", feature = "kafka", feature = "rabbitmq"))]
pub(crate) fn retryable(context: &str, err: impl fmt::Display) -> TransportError {
TransportError::retryable(format!("{context}: {err}"))
}
impl fmt::Display for TransportError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let kind = match self.kind {
TransportErrorKind::Retryable => "retryable",
TransportErrorKind::Permanent => "permanent",
};
write!(f, "transport error ({kind}): {}", self.message)
}
}
impl Error for TransportError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
self.source
.as_ref()
.map(|source| source.as_ref() as &(dyn Error + 'static))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn retryable_and_permanent_classify_themselves() {
let retry = TransportError::retryable("connection reset");
assert!(retry.is_retryable());
assert!(!retry.is_permanent());
assert_eq!(retry.kind(), TransportErrorKind::Retryable);
let permanent = TransportError::permanent("bad payload");
assert!(permanent.is_permanent());
assert!(!permanent.is_retryable());
assert_eq!(permanent.kind(), TransportErrorKind::Permanent);
}
#[test]
fn display_includes_classification_and_message() {
assert_eq!(
TransportError::retryable("lease lost").to_string(),
"transport error (retryable): lease lost"
);
assert_eq!(
TransportError::permanent("decode failed").to_string(),
"transport error (permanent): decode failed"
);
}
#[test]
fn with_source_is_exposed_through_error_trait() {
let inner = std::io::Error::new(std::io::ErrorKind::TimedOut, "timed out");
let err = TransportError::retryable("publish timed out").with_source(inner);
assert!(err.source().is_some());
assert_eq!(err.message(), "publish timed out");
}
}