Skip to main content

nodedb_types/error/
types.rs

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