Skip to main content

klauthed_data/error/
data_error.rs

1use klauthed_core::config::{CacheBackend, DbSystem, MessagingBackend};
2use klauthed_core::error::ConfigError;
3use klauthed_error::{DomainError, ErrorCategory, ErrorCode};
4use thiserror::Error;
5
6/// Errors raised while turning configuration into live data connections.
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum DataError {
10    /// A configuration error surfaced while building a resource. Its category
11    /// and code delegate to the underlying [`ConfigError`].
12    #[error("configuration error: {0}")]
13    Config(#[from] ConfigError),
14
15    /// The configured database system has no connector in this build.
16    #[error("database system '{0:?}' is not supported by this connector")]
17    UnsupportedSystem(DbSystem),
18
19    /// The configured cache backend has no connector in this build.
20    #[error("cache backend '{0:?}' is not supported by this connector")]
21    UnsupportedCacheBackend(CacheBackend),
22
23    /// The configured messaging backend has no connector in this build.
24    #[error("messaging backend '{0:?}' is not supported by this connector")]
25    UnsupportedMessagingBackend(MessagingBackend),
26
27    /// No connection URL could be derived from the provided configuration.
28    #[error("no connection URL could be derived for {0}")]
29    MissingUrl(&'static str),
30
31    /// A messaging client failed to set up or connect.
32    #[error("messaging setup error: {0}")]
33    Messaging(String),
34
35    /// The transactional outbox encountered an error.
36    #[error("transactional outbox error: {0}")]
37    Outbox(String),
38
39    /// The idempotency store encountered an error.
40    #[error("idempotency store error: {0}")]
41    Idempotency(String),
42
43    /// A named distributed lock is already held by another holder.
44    #[error("lock '{0}' is already held")]
45    LockHeld(String),
46
47    /// The requested backend needs a cargo feature that is not enabled.
48    #[error("this backend requires the '{0}' cargo feature to be enabled")]
49    FeatureDisabled(&'static str),
50
51    /// An underlying I/O error.
52    #[error("I/O error: {0}")]
53    Io(#[from] std::io::Error),
54
55    /// A SQL driver error (`sql` feature).
56    #[cfg(feature = "sql")]
57    #[error("database error: {0}")]
58    Sqlx(#[from] sqlx::Error),
59
60    /// A Redis client error (`redis` feature).
61    #[cfg(feature = "redis")]
62    #[error("redis error: {0}")]
63    Redis(#[from] ::redis::RedisError),
64
65    /// A NATS connection error (`nats` feature).
66    #[cfg(feature = "nats")]
67    #[error("nats connection error: {0}")]
68    Nats(#[from] async_nats::ConnectError),
69
70    /// A RabbitMQ connection error (`rabbitmq` feature).
71    #[cfg(feature = "rabbitmq")]
72    #[error("rabbitmq connection error: {0}")]
73    RabbitMq(#[from] lapin::Error),
74
75    /// A Kafka connection error (`kafka` feature).
76    #[cfg(feature = "kafka")]
77    #[error("kafka connection error: {0}")]
78    Kafka(#[from] rskafka::client::error::Error),
79
80    /// An object-storage error (`storage` feature).
81    #[cfg(feature = "storage")]
82    #[error("storage error: {0}")]
83    Storage(#[from] object_store::Error),
84
85    /// A pagination request was malformed (e.g. an out-of-range page size).
86    #[error("invalid pagination request: {0}")]
87    InvalidPage(String),
88
89    /// A pagination cursor was invalid or could not be decoded.
90    #[error("invalid or corrupted cursor: {0}")]
91    InvalidCursor(String),
92}
93
94impl DataError {
95    /// Map a variant to its category and stable code. A wrapped [`ConfigError`]
96    /// delegates to that error's own classification so codes stay accurate
97    /// across crate boundaries.
98    fn classify(&self) -> (ErrorCategory, ErrorCode) {
99        use ErrorCategory::Internal;
100        match self {
101            DataError::Config(e) => (e.category(), e.code()),
102            DataError::UnsupportedSystem(_) => {
103                (Internal, ErrorCode::new("data.unsupported_system"))
104            }
105            DataError::UnsupportedCacheBackend(_) => {
106                (Internal, ErrorCode::new("data.unsupported_cache_backend"))
107            }
108            DataError::UnsupportedMessagingBackend(_) => {
109                (Internal, ErrorCode::new("data.unsupported_messaging_backend"))
110            }
111            DataError::MissingUrl(_) => (Internal, ErrorCode::new("data.missing_url")),
112            DataError::Messaging(_) => (Internal, ErrorCode::new("data.messaging")),
113            DataError::Outbox(_) => (Internal, ErrorCode::new("data.outbox")),
114            DataError::Idempotency(_) => (Internal, ErrorCode::new("data.idempotency")),
115            // Another holder owns the lock — a conflict from the caller's view.
116            DataError::LockHeld(_) => (ErrorCategory::Conflict, ErrorCode::new("data.lock_held")),
117            DataError::FeatureDisabled(_) => (Internal, ErrorCode::new("data.feature_disabled")),
118            DataError::Io(_) => (Internal, ErrorCode::new("data.io")),
119            // Connection/transport failures are transient from the caller's view.
120            #[cfg(feature = "sql")]
121            DataError::Sqlx(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.database")),
122            #[cfg(feature = "redis")]
123            DataError::Redis(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.redis")),
124            #[cfg(feature = "nats")]
125            DataError::Nats(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.nats")),
126            #[cfg(feature = "rabbitmq")]
127            DataError::RabbitMq(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.rabbitmq")),
128            #[cfg(feature = "kafka")]
129            DataError::Kafka(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.kafka")),
130            #[cfg(feature = "storage")]
131            DataError::Storage(_) => (Internal, ErrorCode::new("data.storage")),
132            DataError::InvalidPage(_) => {
133                (ErrorCategory::BadRequest, ErrorCode::new("data.invalid_page"))
134            }
135            DataError::InvalidCursor(_) => {
136                (ErrorCategory::BadRequest, ErrorCode::new("data.invalid_cursor"))
137            }
138        }
139    }
140}
141
142impl DomainError for DataError {
143    fn category(&self) -> ErrorCategory {
144        self.classify().0
145    }
146
147    fn code(&self) -> ErrorCode {
148        self.classify().1
149    }
150}
151
152#[cfg(test)]
153mod tests {
154    use super::*;
155
156    #[test]
157    fn unsupported_system_is_internal() {
158        let err = DataError::UnsupportedSystem(DbSystem::MongoDb);
159        assert_eq!(err.category(), ErrorCategory::Internal);
160        assert_eq!(err.code().as_str(), "data.unsupported_system");
161    }
162
163    #[test]
164    fn wrapped_config_error_delegates_classification() {
165        let err: DataError = ConfigError::MissingRequired("database".into()).into();
166        // Category and code come from the inner ConfigError, not from data.
167        assert_eq!(err.category(), ErrorCategory::Internal);
168        assert_eq!(err.code().as_str(), "config.missing_required");
169    }
170}