use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum ActrError {
#[error("unavailable: {0}")]
Unavailable(String),
#[error("timed out")]
TimedOut,
#[error("not found: {0}")]
NotFound(String),
#[error("permission denied: {0}")]
PermissionDenied(String),
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("unknown route: {0}")]
UnknownRoute(String),
#[error("dependency '{service_name}' not found: {message}")]
DependencyNotFound {
service_name: String,
message: String,
},
#[error("decode failure: {0}")]
DecodeFailure(String),
#[error("not implemented: {0}")]
NotImplemented(String),
#[error("internal error: {0}")]
Internal(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorKind {
Transient,
Client,
Internal,
Corrupt,
}
pub trait Classify {
fn kind(&self) -> ErrorKind;
fn is_retryable(&self) -> bool {
matches!(self.kind(), ErrorKind::Transient)
}
fn requires_dlq(&self) -> bool {
matches!(self.kind(), ErrorKind::Corrupt)
}
}
impl Classify for ActrError {
fn kind(&self) -> ErrorKind {
match self {
ActrError::Unavailable(_) | ActrError::TimedOut => ErrorKind::Transient,
ActrError::NotFound(_)
| ActrError::PermissionDenied(_)
| ActrError::InvalidArgument(_)
| ActrError::UnknownRoute(_)
| ActrError::DependencyNotFound { .. } => ErrorKind::Client,
ActrError::DecodeFailure(_) => ErrorKind::Corrupt,
ActrError::NotImplemented(_) | ActrError::Internal(_) => ErrorKind::Internal,
}
}
}
pub type ActorResult<T> = Result<T, ActrError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn transient_variants_classify_correctly() {
assert_eq!(
ActrError::Unavailable("x".into()).kind(),
ErrorKind::Transient
);
assert_eq!(ActrError::TimedOut.kind(), ErrorKind::Transient);
}
#[test]
fn client_variants_classify_correctly() {
assert_eq!(ActrError::NotFound("x".into()).kind(), ErrorKind::Client);
assert_eq!(
ActrError::PermissionDenied("x".into()).kind(),
ErrorKind::Client
);
assert_eq!(
ActrError::InvalidArgument("x".into()).kind(),
ErrorKind::Client
);
assert_eq!(
ActrError::UnknownRoute("x".into()).kind(),
ErrorKind::Client
);
assert_eq!(
ActrError::DependencyNotFound {
service_name: "svc".into(),
message: "not found".into(),
}
.kind(),
ErrorKind::Client
);
}
#[test]
fn corrupt_variant_classifies_correctly() {
assert_eq!(
ActrError::DecodeFailure("x".into()).kind(),
ErrorKind::Corrupt
);
}
#[test]
fn internal_variants_classify_correctly() {
assert_eq!(
ActrError::NotImplemented("x".into()).kind(),
ErrorKind::Internal
);
assert_eq!(ActrError::Internal("x".into()).kind(), ErrorKind::Internal);
}
#[test]
fn only_transient_is_retryable() {
assert!(ActrError::Unavailable("x".into()).is_retryable());
assert!(ActrError::TimedOut.is_retryable());
assert!(!ActrError::NotFound("x".into()).is_retryable());
assert!(!ActrError::DecodeFailure("x".into()).is_retryable());
assert!(!ActrError::Internal("x".into()).is_retryable());
}
#[test]
fn only_corrupt_requires_dlq() {
assert!(ActrError::DecodeFailure("x".into()).requires_dlq());
assert!(!ActrError::Unavailable("x".into()).requires_dlq());
assert!(!ActrError::TimedOut.requires_dlq());
assert!(!ActrError::NotFound("x".into()).requires_dlq());
assert!(!ActrError::Internal("x".into()).requires_dlq());
}
#[test]
fn actr_error_is_clone() {
let e = ActrError::InvalidArgument("bad".into());
let cloned = e.clone();
assert_eq!(format!("{cloned}"), "invalid argument: bad");
}
}