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 schema migration failed or is invalid (e.g. a duplicate version).
44    #[error("migration error: {0}")]
45    Migration(String),
46
47    /// A named distributed lock is already held by another holder.
48    #[error("lock '{0}' is already held")]
49    LockHeld(String),
50
51    /// The requested backend needs a cargo feature that is not enabled.
52    #[error("this backend requires the '{0}' cargo feature to be enabled")]
53    FeatureDisabled(&'static str),
54
55    /// An underlying I/O error.
56    #[error("I/O error: {0}")]
57    Io(#[from] std::io::Error),
58
59    /// A SQL driver error (`sql` feature).
60    #[cfg(feature = "sql")]
61    #[error("database error: {0}")]
62    Sqlx(#[from] sqlx::Error),
63
64    /// A Redis client error (`redis` feature).
65    #[cfg(feature = "redis")]
66    #[error("redis error: {0}")]
67    Redis(#[from] ::redis::RedisError),
68
69    /// A NATS connection error (`nats` feature).
70    #[cfg(feature = "nats")]
71    #[error("nats connection error: {0}")]
72    Nats(#[from] async_nats::ConnectError),
73
74    /// A RabbitMQ connection error (`rabbitmq` feature).
75    #[cfg(feature = "rabbitmq")]
76    #[error("rabbitmq connection error: {0}")]
77    RabbitMq(#[from] lapin::Error),
78
79    /// A Kafka connection error (`kafka` feature).
80    #[cfg(feature = "kafka")]
81    #[error("kafka connection error: {0}")]
82    Kafka(#[from] rskafka::client::error::Error),
83
84    /// An object-storage error (`storage` feature).
85    #[cfg(feature = "storage")]
86    #[error("storage error: {0}")]
87    Storage(#[from] object_store::Error),
88
89    /// A pagination request was malformed (e.g. an out-of-range page size).
90    #[error("invalid pagination request: {0}")]
91    InvalidPage(String),
92
93    /// A pagination cursor was invalid or could not be decoded.
94    #[error("invalid or corrupted cursor: {0}")]
95    InvalidCursor(String),
96}
97
98impl DataError {
99    /// Map a variant to its category and stable code. A wrapped [`ConfigError`]
100    /// delegates to that error's own classification so codes stay accurate
101    /// across crate boundaries.
102    fn classify(&self) -> (ErrorCategory, ErrorCode) {
103        use ErrorCategory::Internal;
104        match self {
105            DataError::Config(e) => (e.category(), e.code()),
106            DataError::UnsupportedSystem(_) => {
107                (Internal, ErrorCode::new("data.unsupported_system"))
108            }
109            DataError::UnsupportedCacheBackend(_) => {
110                (Internal, ErrorCode::new("data.unsupported_cache_backend"))
111            }
112            DataError::UnsupportedMessagingBackend(_) => {
113                (Internal, ErrorCode::new("data.unsupported_messaging_backend"))
114            }
115            DataError::MissingUrl(_) => (Internal, ErrorCode::new("data.missing_url")),
116            DataError::Messaging(_) => (Internal, ErrorCode::new("data.messaging")),
117            DataError::Outbox(_) => (Internal, ErrorCode::new("data.outbox")),
118            DataError::Idempotency(_) => (Internal, ErrorCode::new("data.idempotency")),
119            DataError::Migration(_) => (Internal, ErrorCode::new("data.migration")),
120            // Another holder owns the lock — a conflict from the caller's view.
121            DataError::LockHeld(_) => (ErrorCategory::Conflict, ErrorCode::new("data.lock_held")),
122            DataError::FeatureDisabled(_) => (Internal, ErrorCode::new("data.feature_disabled")),
123            DataError::Io(_) => (Internal, ErrorCode::new("data.io")),
124            // Connection/transport failures are transient from the caller's view.
125            #[cfg(feature = "sql")]
126            DataError::Sqlx(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.database")),
127            #[cfg(feature = "redis")]
128            DataError::Redis(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.redis")),
129            #[cfg(feature = "nats")]
130            DataError::Nats(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.nats")),
131            #[cfg(feature = "rabbitmq")]
132            DataError::RabbitMq(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.rabbitmq")),
133            #[cfg(feature = "kafka")]
134            DataError::Kafka(_) => (ErrorCategory::Unavailable, ErrorCode::new("data.kafka")),
135            #[cfg(feature = "storage")]
136            DataError::Storage(_) => (Internal, ErrorCode::new("data.storage")),
137            DataError::InvalidPage(_) => {
138                (ErrorCategory::BadRequest, ErrorCode::new("data.invalid_page"))
139            }
140            DataError::InvalidCursor(_) => {
141                (ErrorCategory::BadRequest, ErrorCode::new("data.invalid_cursor"))
142            }
143        }
144    }
145}
146
147impl DomainError for DataError {
148    fn category(&self) -> ErrorCategory {
149        self.classify().0
150    }
151
152    fn code(&self) -> ErrorCode {
153        self.classify().1
154    }
155}
156
157#[cfg(test)]
158mod tests {
159    use super::*;
160
161    #[test]
162    fn unsupported_system_is_internal() {
163        let err = DataError::UnsupportedSystem(DbSystem::MongoDb);
164        assert_eq!(err.category(), ErrorCategory::Internal);
165        assert_eq!(err.code().as_str(), "data.unsupported_system");
166    }
167
168    #[test]
169    fn wrapped_config_error_delegates_classification() {
170        let err: DataError = ConfigError::MissingRequired("database".into()).into();
171        // Category and code come from the inner ConfigError, not from data.
172        assert_eq!(err.category(), ErrorCategory::Internal);
173        assert_eq!(err.code().as_str(), "config.missing_required");
174    }
175}