#[cfg(feature = "schema")]
use std::fmt;
pub type Result<T> = std::result::Result<T, PubSubError>;
#[derive(Debug, thiserror::Error)]
pub enum PubSubError {
#[error("Publishing error: {message}")]
PublishError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Subscription error: {message}")]
SubscriptionError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Acknowledgment error: {message}")]
AcknowledgmentError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[cfg(feature = "schema")]
#[error("Schema validation error: {message}")]
SchemaValidationError {
message: String,
schema_id: Option<String>,
},
#[cfg(feature = "schema")]
#[error("Schema encoding error: {message}")]
SchemaEncodingError {
message: String,
format: SchemaFormat,
},
#[cfg(feature = "schema")]
#[error("Schema decoding error: {message}")]
SchemaDecodingError {
message: String,
format: SchemaFormat,
},
#[error("Batching error: {message}")]
BatchingError {
message: String,
batch_size: usize,
},
#[error("Flow control error: {message}")]
FlowControlError {
message: String,
current_count: usize,
max_count: usize,
},
#[error("Dead letter queue error: {message}")]
DeadLetterQueueError {
message: String,
message_id: String,
},
#[error("Ordering key error: {message}")]
OrderingKeyError {
message: String,
ordering_key: String,
},
#[cfg(feature = "monitoring")]
#[error("Monitoring error: {message}")]
MonitoringError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Authentication error: {message}")]
AuthenticationError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Configuration error: {message}")]
ConfigurationError {
message: String,
parameter: String,
},
#[error("Topic not found: {topic_name}")]
TopicNotFound {
topic_name: String,
},
#[error("Subscription not found: {subscription_name}")]
SubscriptionNotFound {
subscription_name: String,
},
#[error("Message too large: {size} bytes (max: {max_size} bytes)")]
MessageTooLarge {
size: usize,
max_size: usize,
},
#[error("Invalid message format: {message}")]
InvalidMessageFormat {
message: String,
},
#[error("Operation timed out after {duration_ms}ms")]
Timeout {
duration_ms: u64,
},
#[error("Network error: {message}")]
NetworkError {
message: String,
#[source]
source: Option<Box<dyn std::error::Error + Send + Sync>>,
},
#[error("Resource exhausted: {resource}")]
ResourceExhausted {
resource: String,
retry_after: Option<u64>,
},
#[error("Permission denied: {operation}")]
PermissionDenied {
operation: String,
},
#[error("Internal error: {message}")]
InternalError {
message: String,
},
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Pub/Sub client error: {0}")]
ClientError(String),
}
#[cfg(feature = "schema")]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SchemaFormat {
Avro,
Protobuf,
}
#[cfg(feature = "schema")]
impl fmt::Display for SchemaFormat {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
SchemaFormat::Avro => write!(f, "Avro"),
SchemaFormat::Protobuf => write!(f, "Protobuf"),
}
}
}
impl PubSubError {
pub fn publish<S: Into<String>>(message: S) -> Self {
Self::PublishError {
message: message.into(),
source: None,
}
}
pub fn publish_with_source<S: Into<String>>(
message: S,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::PublishError {
message: message.into(),
source: Some(source),
}
}
pub fn subscription<S: Into<String>>(message: S) -> Self {
Self::SubscriptionError {
message: message.into(),
source: None,
}
}
pub fn subscription_with_source<S: Into<String>>(
message: S,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::SubscriptionError {
message: message.into(),
source: Some(source),
}
}
pub fn acknowledgment<S: Into<String>>(message: S) -> Self {
Self::AcknowledgmentError {
message: message.into(),
source: None,
}
}
pub fn configuration<S: Into<String>, P: Into<String>>(message: S, parameter: P) -> Self {
Self::ConfigurationError {
message: message.into(),
parameter: parameter.into(),
}
}
pub fn batching<S: Into<String>>(message: S, batch_size: usize) -> Self {
Self::BatchingError {
message: message.into(),
batch_size,
}
}
pub fn flow_control<S: Into<String>>(
message: S,
current_count: usize,
max_count: usize,
) -> Self {
Self::FlowControlError {
message: message.into(),
current_count,
max_count,
}
}
pub fn dead_letter<S: Into<String>, M: Into<String>>(message: S, message_id: M) -> Self {
Self::DeadLetterQueueError {
message: message.into(),
message_id: message_id.into(),
}
}
pub fn ordering_key<S: Into<String>, K: Into<String>>(message: S, ordering_key: K) -> Self {
Self::OrderingKeyError {
message: message.into(),
ordering_key: ordering_key.into(),
}
}
pub fn topic_not_found<S: Into<String>>(topic_name: S) -> Self {
Self::TopicNotFound {
topic_name: topic_name.into(),
}
}
pub fn subscription_not_found<S: Into<String>>(subscription_name: S) -> Self {
Self::SubscriptionNotFound {
subscription_name: subscription_name.into(),
}
}
pub fn message_too_large(size: usize, max_size: usize) -> Self {
Self::MessageTooLarge { size, max_size }
}
pub fn timeout(duration_ms: u64) -> Self {
Self::Timeout { duration_ms }
}
pub fn resource_exhausted<S: Into<String>>(resource: S, retry_after: Option<u64>) -> Self {
Self::ResourceExhausted {
resource: resource.into(),
retry_after,
}
}
pub fn permission_denied<S: Into<String>>(operation: S) -> Self {
Self::PermissionDenied {
operation: operation.into(),
}
}
pub fn is_retryable(&self) -> bool {
matches!(
self,
Self::NetworkError { .. }
| Self::ResourceExhausted { .. }
| Self::Timeout { .. }
| Self::InternalError { .. }
)
}
pub fn retry_after(&self) -> Option<u64> {
match self {
Self::ResourceExhausted { retry_after, .. } => *retry_after,
_ => None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_publish_error_creation() {
let error = PubSubError::publish("test error");
assert!(matches!(error, PubSubError::PublishError { .. }));
assert!(error.to_string().contains("test error"));
}
#[test]
fn test_subscription_error_creation() {
let error = PubSubError::subscription("test error");
assert!(matches!(error, PubSubError::SubscriptionError { .. }));
assert!(error.to_string().contains("test error"));
}
#[test]
fn test_configuration_error() {
let error = PubSubError::configuration("invalid value", "timeout");
assert!(matches!(error, PubSubError::ConfigurationError { .. }));
assert!(error.to_string().contains("invalid value"));
}
#[test]
fn test_message_too_large_error() {
let error = PubSubError::message_too_large(11000000, 10000000);
assert!(matches!(error, PubSubError::MessageTooLarge { .. }));
assert!(error.to_string().contains("11000000"));
assert!(error.to_string().contains("10000000"));
}
#[test]
fn test_retryable_errors() {
let network_error = PubSubError::NetworkError {
message: "connection reset".to_string(),
source: None,
};
assert!(network_error.is_retryable());
let timeout_error = PubSubError::timeout(5000);
assert!(timeout_error.is_retryable());
let config_error = PubSubError::configuration("bad value", "param");
assert!(!config_error.is_retryable());
}
#[test]
fn test_retry_after() {
let error = PubSubError::resource_exhausted("quota", Some(60));
assert_eq!(error.retry_after(), Some(60));
let error = PubSubError::timeout(1000);
assert_eq!(error.retry_after(), None);
}
#[test]
fn test_topic_not_found() {
let error = PubSubError::topic_not_found("my-topic");
assert!(matches!(error, PubSubError::TopicNotFound { .. }));
assert!(error.to_string().contains("my-topic"));
}
#[test]
fn test_flow_control_error() {
let error = PubSubError::flow_control("limit exceeded", 1000, 500);
assert!(matches!(error, PubSubError::FlowControlError { .. }));
assert!(error.to_string().contains("limit exceeded"));
}
}