Skip to main content

nodedb_types/error/
types.rs

1//! [`NodeDbError`] struct + accessors + category predicates + `From<io::Error>`.
2
3use std::fmt;
4
5use serde::{Deserialize, Serialize};
6
7use super::code::ErrorCode;
8use super::details::ErrorDetails;
9
10/// Public error type returned by all `NodeDb` trait methods.
11///
12/// Separates machine-readable data ([`ErrorCode`] + [`ErrorDetails`]) from
13/// the human-readable `message`. Optional `cause` preserves the error chain.
14#[derive(Debug, Clone, Serialize, Deserialize)]
15pub struct NodeDbError {
16    pub(super) code: ErrorCode,
17    pub(super) message: String,
18    pub(super) details: ErrorDetails,
19    #[serde(skip_serializing_if = "Option::is_none")]
20    pub(super) cause: Option<Box<NodeDbError>>,
21}
22
23impl fmt::Display for NodeDbError {
24    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
25        write!(f, "[{}] {}", self.code, self.message)
26    }
27}
28
29impl std::error::Error for NodeDbError {
30    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
31        self.cause.as_deref().map(|e| e as &dyn std::error::Error)
32    }
33}
34
35// ── Accessors ──
36
37impl NodeDbError {
38    /// The stable numeric error code.
39    pub fn code(&self) -> ErrorCode {
40        self.code
41    }
42
43    /// Human-readable error message.
44    pub fn message(&self) -> &str {
45        &self.message
46    }
47
48    /// Machine-matchable error details.
49    pub fn details(&self) -> &ErrorDetails {
50        &self.details
51    }
52
53    /// The chained cause, if any.
54    pub fn cause(&self) -> Option<&NodeDbError> {
55        self.cause.as_deref()
56    }
57
58    /// Attach a cause to this error.
59    pub fn with_cause(mut self, cause: NodeDbError) -> Self {
60        self.cause = Some(Box::new(cause));
61        self
62    }
63
64    /// Whether this error is retriable by the client.
65    pub fn is_retriable(&self) -> bool {
66        matches!(
67            self.details,
68            ErrorDetails::WriteConflict { .. }
69                | ErrorDetails::DeadlineExceeded
70                | ErrorDetails::NoLeader
71                | ErrorDetails::NotLeader { .. }
72                | ErrorDetails::MigrationInProgress
73                | ErrorDetails::NodeUnreachable
74                | ErrorDetails::SyncConnectionFailed
75                | ErrorDetails::Bridge
76                | ErrorDetails::MemoryExhausted { .. }
77        )
78    }
79
80    /// Whether this error indicates the client sent invalid input.
81    pub fn is_client_error(&self) -> bool {
82        matches!(
83            self.details,
84            ErrorDetails::BadRequest
85                | ErrorDetails::ConstraintViolation { .. }
86                | ErrorDetails::AppendOnlyViolation { .. }
87                | ErrorDetails::BalanceViolation { .. }
88                | ErrorDetails::PeriodLocked { .. }
89                | ErrorDetails::StateTransitionViolation { .. }
90                | ErrorDetails::TransitionCheckViolation { .. }
91                | ErrorDetails::RetentionViolation { .. }
92                | ErrorDetails::LegalHoldActive { .. }
93                | ErrorDetails::CollectionNotFound { .. }
94                | ErrorDetails::DocumentNotFound { .. }
95                | ErrorDetails::AuthorizationDenied { .. }
96                | ErrorDetails::AuthExpired
97                | ErrorDetails::Config
98                | ErrorDetails::SqlNotEnabled
99        )
100    }
101}
102
103// ── Category predicates ──
104
105impl NodeDbError {
106    pub fn is_constraint_violation(&self) -> bool {
107        matches!(self.details, ErrorDetails::ConstraintViolation { .. })
108    }
109    pub fn is_not_found(&self) -> bool {
110        matches!(
111            self.details,
112            ErrorDetails::CollectionNotFound { .. } | ErrorDetails::DocumentNotFound { .. }
113        )
114    }
115    pub fn is_auth_denied(&self) -> bool {
116        matches!(self.details, ErrorDetails::AuthorizationDenied { .. })
117    }
118    pub fn is_storage(&self) -> bool {
119        matches!(
120            self.details,
121            ErrorDetails::Storage
122                | ErrorDetails::SegmentCorrupted
123                | ErrorDetails::ColdStorage
124                | ErrorDetails::Wal
125        )
126    }
127    pub fn is_internal(&self) -> bool {
128        matches!(self.details, ErrorDetails::Internal)
129    }
130    pub fn is_type_mismatch(&self) -> bool {
131        matches!(self.details, ErrorDetails::TypeMismatch { .. })
132    }
133    pub fn is_type_guard_violation(&self) -> bool {
134        matches!(self.details, ErrorDetails::TypeGuardViolation { .. })
135    }
136    pub fn is_overflow(&self) -> bool {
137        matches!(self.details, ErrorDetails::Overflow { .. })
138    }
139    pub fn is_insufficient_balance(&self) -> bool {
140        matches!(self.details, ErrorDetails::InsufficientBalance { .. })
141    }
142    pub fn is_rate_exceeded(&self) -> bool {
143        matches!(self.details, ErrorDetails::RateExceeded { .. })
144    }
145    pub fn is_cluster(&self) -> bool {
146        matches!(
147            self.details,
148            ErrorDetails::NoLeader
149                | ErrorDetails::NotLeader { .. }
150                | ErrorDetails::MigrationInProgress
151                | ErrorDetails::NodeUnreachable
152                | ErrorDetails::Cluster
153        )
154    }
155}
156
157/// Result alias for NodeDb operations.
158pub type NodeDbResult<T> = std::result::Result<T, NodeDbError>;
159
160impl From<std::io::Error> for NodeDbError {
161    fn from(e: std::io::Error) -> Self {
162        Self::storage(e)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn error_display_includes_code() {
172        let e = NodeDbError::constraint_violation("users", "duplicate email");
173        let msg = e.to_string();
174        assert!(msg.contains("NDB-1000"));
175        assert!(msg.contains("constraint violation"));
176        assert!(msg.contains("users"));
177    }
178
179    #[test]
180    fn error_code_accessor() {
181        let e = NodeDbError::write_conflict("orders", "abc");
182        assert_eq!(e.code(), ErrorCode::WRITE_CONFLICT);
183        assert_eq!(e.code().0, 1001);
184    }
185
186    #[test]
187    fn details_matching() {
188        let e = NodeDbError::collection_not_found("users");
189        assert!(matches!(
190            e.details(),
191            ErrorDetails::CollectionNotFound { collection } if collection == "users"
192        ));
193        assert!(e.is_not_found());
194    }
195
196    #[test]
197    fn error_cause_chaining() {
198        let inner = NodeDbError::storage("disk full");
199        let outer = NodeDbError::internal("write failed").with_cause(inner);
200        assert!(outer.cause().is_some());
201        assert!(outer.cause().unwrap().is_storage());
202        assert!(outer.cause().unwrap().message().contains("disk full"));
203    }
204
205    #[test]
206    fn io_error_converts() {
207        let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
208        let e: NodeDbError = io_err.into();
209        assert!(e.is_storage());
210        assert_eq!(e.code(), ErrorCode::STORAGE);
211    }
212
213    #[test]
214    fn retriable_errors() {
215        assert!(NodeDbError::write_conflict("x", "y").is_retriable());
216        assert!(NodeDbError::deadline_exceeded().is_retriable());
217        assert!(!NodeDbError::bad_request("bad").is_retriable());
218    }
219
220    #[test]
221    fn client_errors() {
222        assert!(NodeDbError::bad_request("bad").is_client_error());
223        assert!(!NodeDbError::internal("oops").is_client_error());
224    }
225
226    #[test]
227    fn json_serialization() {
228        let e = NodeDbError::collection_not_found("users");
229        let json = serde_json::to_value(&e).unwrap();
230        assert_eq!(json["code"], 1100);
231        assert!(json["message"].as_str().unwrap().contains("users"));
232        assert_eq!(json["details"]["kind"], "collection_not_found");
233        assert_eq!(json["details"]["collection"], "users");
234    }
235
236    #[test]
237    fn sync_delta_rejected_ctor() {
238        let e = NodeDbError::sync_delta_rejected(
239            "unique violation",
240            Some(
241                crate::sync::compensation::CompensationHint::UniqueViolation {
242                    field: "email".into(),
243                    conflicting_value: "a@b.com".into(),
244                },
245            ),
246        );
247        assert!(e.to_string().contains("NDB-3001"));
248        assert!(e.to_string().contains("sync delta rejected"));
249    }
250
251    #[test]
252    fn sql_not_enabled_ctor() {
253        let e = NodeDbError::sql_not_enabled();
254        assert!(e.to_string().contains("SQL not enabled"));
255        assert_eq!(e.code(), ErrorCode::SQL_NOT_ENABLED);
256    }
257}