1use std::io;
8
9use thiserror::Error;
10
11#[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 #[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 #[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}