#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ProviderError {
pub operation: String,
pub message: String,
pub retryable: bool,
}
impl ProviderError {
pub fn retryable(operation: impl Into<String>, message: impl Into<String>) -> Self {
Self {
operation: operation.into(),
message: message.into(),
retryable: true,
}
}
pub fn permanent(operation: impl Into<String>, message: impl Into<String>) -> Self {
Self {
operation: operation.into(),
message: message.into(),
retryable: false,
}
}
pub fn is_retryable(&self) -> bool {
self.retryable
}
pub fn to_infrastructure_error(&self) -> crate::ErrorDetails {
crate::ErrorDetails::Infrastructure {
operation: self.operation.clone(),
message: self.message.clone(),
retryable: self.retryable,
}
}
}
impl std::fmt::Display for ProviderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}: {}", self.operation, self.message)
}
}
impl std::error::Error for ProviderError {}
impl From<String> for ProviderError {
fn from(s: String) -> Self {
Self {
operation: "unknown".to_string(),
message: s,
retryable: true, }
}
}
impl From<&str> for ProviderError {
fn from(s: &str) -> Self {
s.to_string().into()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_provider_error_classification() {
let retryable = ProviderError::retryable("fetch_orchestration_item", "Database is busy");
assert!(retryable.is_retryable(), "Retryable error should be retryable");
assert_eq!(retryable.operation, "fetch_orchestration_item");
assert!(retryable.message.contains("busy"));
let permanent = ProviderError::permanent("ack_orchestration_item", "Duplicate event detected");
assert!(!permanent.is_retryable(), "Permanent error should not be retryable");
assert_eq!(permanent.operation, "ack_orchestration_item");
assert!(permanent.message.contains("Duplicate"));
let display = format!("{permanent}");
assert!(display.contains("ack_orchestration_item"));
assert!(display.contains("Duplicate"));
let _err: Box<dyn std::error::Error> = Box::new(permanent.clone());
}
#[test]
fn test_provider_error_from_string() {
let from_string: ProviderError = "Some error message".into();
assert!(
from_string.is_retryable(),
"String errors should be retryable by default"
);
assert_eq!(from_string.operation, "unknown");
assert_eq!(from_string.message, "Some error message");
let from_owned: ProviderError = String::from("Another error").into();
assert!(from_owned.is_retryable());
assert_eq!(from_owned.message, "Another error");
}
#[test]
fn test_provider_error_to_infrastructure() {
let retryable = ProviderError::retryable("read", "Connection timeout");
let infra = retryable.to_infrastructure_error();
match infra {
crate::ErrorDetails::Infrastructure {
operation,
message,
retryable,
} => {
assert_eq!(operation, "read");
assert!(message.contains("timeout"));
assert!(retryable);
}
_ => panic!("Expected Infrastructure error"),
}
let permanent = ProviderError::permanent("write", "Data corruption");
let infra = permanent.to_infrastructure_error();
match infra {
crate::ErrorDetails::Infrastructure {
operation,
message,
retryable,
} => {
assert_eq!(operation, "write");
assert!(message.contains("corruption"));
assert!(!retryable);
}
_ => panic!("Expected Infrastructure error"),
}
}
#[test]
fn test_provider_error_equality() {
let err1 = ProviderError::retryable("op", "msg");
let err2 = ProviderError::retryable("op", "msg");
let err3 = ProviderError::permanent("op", "msg");
assert_eq!(err1, err2);
assert_ne!(err1, err3); }
}