Skip to main content

nookdb_core/
error.rs

1//! Error type hierarchy for `nookdb-core`.
2//!
3//! Every public function in this crate returns `Result<_, NookError>`.
4//! The `kind()` accessor gives a stable string discriminant the NAPI
5//! binding uses to translate Rust errors into typed JS error classes.
6
7use std::io;
8
9use thiserror::Error;
10
11/// Stable kind tag used to map a `NookError` to a JS error class.
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum NookErrorKind {
14    Storage,
15    Corruption,
16    Conflict,
17    Transaction,
18    InvalidArg,
19    Closed,
20    Schema,
21    Migration,
22}
23
24impl NookErrorKind {
25    /// Returns the lowercase string slug used in the NAPI message prefix.
26    #[must_use]
27    pub const fn as_str(self) -> &'static str {
28        match self {
29            Self::Storage => "storage",
30            Self::Corruption => "corruption",
31            Self::Conflict => "conflict",
32            Self::Transaction => "transaction",
33            Self::InvalidArg => "invalid_arg",
34            Self::Closed => "closed",
35            Self::Schema => "schema",
36            Self::Migration => "migration",
37        }
38    }
39}
40
41#[derive(Debug, Error)]
42pub enum NookError {
43    #[error("storage error: {0}")]
44    Storage(#[from] io::Error),
45
46    #[error("database corruption: {msg}")]
47    Corruption { msg: String },
48
49    #[error("write conflict: {msg}")]
50    Conflict { msg: String },
51
52    #[error("transaction error: {msg}")]
53    Transaction { msg: String },
54
55    #[error("invalid argument: {msg}")]
56    InvalidArg { msg: String },
57
58    #[error("database is closed")]
59    Closed,
60
61    #[error("schema error: {msg}")]
62    Schema { msg: String },
63
64    #[error("migration error: {msg}")]
65    Migration { msg: String },
66}
67
68impl NookError {
69    /// Stable discriminant suitable for cross-language error mapping.
70    #[must_use]
71    pub const fn kind(&self) -> NookErrorKind {
72        match self {
73            Self::Storage(_) => NookErrorKind::Storage,
74            Self::Corruption { .. } => NookErrorKind::Corruption,
75            Self::Conflict { .. } => NookErrorKind::Conflict,
76            Self::Transaction { .. } => NookErrorKind::Transaction,
77            Self::InvalidArg { .. } => NookErrorKind::InvalidArg,
78            Self::Closed => NookErrorKind::Closed,
79            Self::Schema { .. } => NookErrorKind::Schema,
80            Self::Migration { .. } => NookErrorKind::Migration,
81        }
82    }
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88
89    #[test]
90    fn kind_str_matches_variant() {
91        assert_eq!(NookErrorKind::Storage.as_str(), "storage");
92        assert_eq!(NookErrorKind::Corruption.as_str(), "corruption");
93        assert_eq!(NookErrorKind::Conflict.as_str(), "conflict");
94        assert_eq!(NookErrorKind::Transaction.as_str(), "transaction");
95        assert_eq!(NookErrorKind::InvalidArg.as_str(), "invalid_arg");
96        assert_eq!(NookErrorKind::Closed.as_str(), "closed");
97        assert_eq!(NookErrorKind::Schema.as_str(), "schema");
98        assert_eq!(NookErrorKind::Migration.as_str(), "migration");
99    }
100
101    #[test]
102    fn display_includes_message() {
103        let e = NookError::InvalidArg {
104            msg: "bad collection".to_string(),
105        };
106        assert_eq!(e.to_string(), "invalid argument: bad collection");
107    }
108
109    #[test]
110    fn io_error_converts_to_storage_variant() {
111        use std::error::Error as _;
112
113        let io_err = io::Error::new(io::ErrorKind::PermissionDenied, "nope");
114        let nook: NookError = io_err.into();
115        assert_eq!(nook.kind(), NookErrorKind::Storage);
116        assert!(nook.to_string().contains("storage error"));
117        assert!(
118            nook.source().is_some(),
119            "Storage variant must chain the inner io::Error as its source",
120        );
121    }
122
123    #[test]
124    fn kind_is_stable_across_variants() {
125        assert_eq!(
126            NookError::Conflict { msg: "x".into() }.kind(),
127            NookErrorKind::Conflict,
128        );
129        assert_eq!(NookError::Closed.kind(), NookErrorKind::Closed);
130        assert_eq!(
131            NookError::Corruption { msg: "x".into() }.kind(),
132            NookErrorKind::Corruption,
133        );
134    }
135
136    #[test]
137    fn error_implements_std_error_trait() {
138        fn assert_error<E: std::error::Error>() {}
139        assert_error::<NookError>();
140    }
141
142    #[test]
143    fn schema_and_migration_kinds_have_stable_slugs() {
144        assert_eq!(NookErrorKind::Schema.as_str(), "schema");
145        assert_eq!(NookErrorKind::Migration.as_str(), "migration");
146    }
147
148    #[test]
149    fn schema_error_carries_message_and_kind() {
150        let e = NookError::Schema {
151            msg: "bad field".into(),
152        };
153        assert_eq!(e.kind(), NookErrorKind::Schema);
154        assert!(e.to_string().contains("bad field"));
155    }
156
157    #[test]
158    fn conflict_error_carries_message_and_kind() {
159        let e = NookError::Conflict {
160            msg: "users.email = a@b".into(),
161        };
162        assert_eq!(e.kind(), NookErrorKind::Conflict);
163        assert!(e.to_string().contains("a@b"));
164    }
165
166    #[test]
167    fn migration_error_carries_message_and_kind() {
168        let e = NookError::Migration {
169            msg: "version gap".into(),
170        };
171        assert_eq!(e.kind(), NookErrorKind::Migration);
172        assert!(e.to_string().contains("version gap"));
173    }
174}