1use std::path::PathBuf;
7
8use thiserror::Error;
9
10use crate::entry::ID;
11
12#[non_exhaustive]
21#[derive(Debug, Error)]
22pub enum InstanceError {
23 #[error("Database not found: {name}")]
25 DatabaseNotFound {
26 name: String,
28 },
29
30 #[error("Database already exists: {name}")]
32 DatabaseAlreadyExists {
33 name: String,
35 },
36
37 #[error("Instance already exists on backend (found device key and system databases)")]
39 InstanceAlreadyExists,
40
41 #[error(
49 "Instance not initialised on this backend; use Instance::connect_or_create to bootstrap"
50 )]
51 NotInitialized,
52
53 #[error("Invalid URL `{url}`: {reason}")]
55 InvalidUrl {
56 url: String,
58 reason: String,
60 },
61
62 #[error("Unsupported URL scheme `{scheme}://`{hint}", hint = match suggested {
64 Some(s) => format!(" — did you mean `{s}://`?"),
65 None => String::new(),
66 })]
67 UnsupportedScheme {
68 scheme: String,
70 suggested: Option<&'static str>,
72 },
73
74 #[error(
76 "Backend for `{scheme}://` is not available in this build; rebuild with `--features {missing_feature}`"
77 )]
78 BackendUnavailable {
79 scheme: &'static str,
81 missing_feature: &'static str,
83 },
84
85 #[error("Invalid snapshot at `{path}`: {reason}")]
87 InvalidSnapshot {
88 path: PathBuf,
90 reason: String,
92 },
93
94 #[error(
96 "Snapshot is only supported on the in-memory backend; use a `memory:///path.json` URL for auto-save on close/Drop, or call `flush()` manually on an in-memory instance"
97 )]
98 SnapshotNotSupported,
99
100 #[error("Entry '{entry_id}' does not belong to database '{database_id}'")]
102 EntryNotInDatabase {
103 entry_id: ID,
105 database_id: ID,
107 },
108
109 #[error("Entry not found: {entry_id}")]
111 EntryNotFound {
112 entry_id: ID,
114 },
115
116 #[error("Transaction has already been committed")]
118 TransactionAlreadyCommitted,
119
120 #[error("Cannot create transaction with empty tips")]
122 EmptyTipsNotAllowed,
123
124 #[error("Tip entry '{tip_id}' does not belong to database '{database_id}'")]
126 InvalidTip {
127 tip_id: ID,
129 database_id: ID,
131 },
132
133 #[error("Signing key '{key_name}' not found in backend")]
135 SigningKeyNotFound {
136 key_name: String,
138 },
139
140 #[error("Authentication required but no key configured")]
142 AuthenticationRequired,
143
144 #[error("Device key not found in instance metadata")]
146 DeviceKeyNotFound,
147
148 #[error("Device key in secrets does not match public key in metadata")]
150 DeviceKeyMismatch,
151
152 #[error("No authentication configuration found")]
154 NoAuthConfiguration,
155
156 #[error("Authentication validation failed: {reason}")]
158 AuthenticationValidationFailed {
159 reason: String,
161 },
162
163 #[error("Insufficient permissions for operation")]
165 InsufficientPermissions,
166
167 #[error("Signature verification failed")]
169 SignatureVerificationFailed,
170
171 #[error("Invalid data type: expected {expected}, got {actual}")]
173 InvalidDataType {
174 expected: String,
176 actual: String,
178 },
179
180 #[error("Serialization failed for {context}")]
182 SerializationFailed {
183 context: String,
185 },
186
187 #[error("Invalid database configuration: {reason}")]
189 InvalidDatabaseConfiguration {
190 reason: String,
192 },
193
194 #[error("Settings validation failed: {reason}")]
196 SettingsValidationFailed {
197 reason: String,
199 },
200
201 #[error("Invalid operation: {reason}")]
203 InvalidOperation {
204 reason: String,
206 },
207
208 #[error("Database initialization failed: {reason}")]
210 DatabaseInitializationFailed {
211 reason: String,
213 },
214
215 #[error("Entry validation failed: {reason}")]
217 EntryValidationFailed {
218 reason: String,
220 },
221
222 #[error("Database state corruption detected: {reason}")]
224 DatabaseStateCorruption {
225 reason: String,
227 },
228
229 #[error("Operation not supported: {operation}")]
231 OperationNotSupported {
232 operation: String,
234 },
235
236 #[error("Instance has been dropped")]
238 InstanceDropped,
239
240 #[error("Sync has already been enabled on this Instance")]
242 SyncAlreadyEnabled,
243
244 #[error("System database not found: {database_name}")]
246 SystemDatabaseNotFound {
247 database_name: String,
249 },
250}
251
252impl InstanceError {
253 pub fn is_not_found(&self) -> bool {
255 matches!(
256 self,
257 InstanceError::DatabaseNotFound { .. }
258 | InstanceError::EntryNotFound { .. }
259 | InstanceError::SigningKeyNotFound { .. }
260 | InstanceError::SystemDatabaseNotFound { .. }
261 )
262 }
263
264 pub fn is_already_exists(&self) -> bool {
266 matches!(
267 self,
268 InstanceError::DatabaseAlreadyExists { .. } | InstanceError::InstanceAlreadyExists
269 )
270 }
271
272 pub fn is_authentication_error(&self) -> bool {
274 matches!(
275 self,
276 InstanceError::AuthenticationRequired
277 | InstanceError::NoAuthConfiguration
278 | InstanceError::AuthenticationValidationFailed { .. }
279 | InstanceError::InsufficientPermissions
280 | InstanceError::SignatureVerificationFailed
281 | InstanceError::SigningKeyNotFound { .. }
282 )
283 }
284
285 pub fn is_operation_error(&self) -> bool {
287 matches!(
288 self,
289 InstanceError::TransactionAlreadyCommitted
290 | InstanceError::EmptyTipsNotAllowed
291 | InstanceError::InvalidOperation { .. }
292 )
293 }
294
295 pub fn is_validation_error(&self) -> bool {
297 matches!(
298 self,
299 InstanceError::EntryNotInDatabase { .. }
300 | InstanceError::InvalidTip { .. }
301 | InstanceError::InvalidDataType { .. }
302 | InstanceError::InvalidDatabaseConfiguration { .. }
303 | InstanceError::SettingsValidationFailed { .. }
304 | InstanceError::EntryValidationFailed { .. }
305 )
306 }
307
308 pub fn is_corruption_error(&self) -> bool {
310 matches!(self, InstanceError::DatabaseStateCorruption { .. })
311 }
312
313 pub fn entry_id(&self) -> Option<&ID> {
315 match self {
316 InstanceError::EntryNotFound { entry_id }
317 | InstanceError::EntryNotInDatabase { entry_id, .. }
318 | InstanceError::InvalidTip {
319 tip_id: entry_id, ..
320 } => Some(entry_id),
321 _ => None,
322 }
323 }
324
325 pub fn database_id(&self) -> Option<&ID> {
327 match self {
328 InstanceError::EntryNotInDatabase { database_id, .. }
329 | InstanceError::InvalidTip { database_id, .. } => Some(database_id),
330 _ => None,
331 }
332 }
333
334 pub fn database_name(&self) -> Option<&str> {
336 match self {
337 InstanceError::DatabaseNotFound { name }
338 | InstanceError::DatabaseAlreadyExists { name } => Some(name),
339 _ => None,
340 }
341 }
342}
343
344impl From<InstanceError> for crate::Error {
346 fn from(err: InstanceError) -> Self {
347 crate::Error::Instance(Box::new(err))
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_error_helpers() {
358 let err = InstanceError::DatabaseNotFound {
359 name: "test-database".to_string(),
360 };
361 assert!(err.is_not_found());
362 assert_eq!(err.database_name(), Some("test-database"));
363
364 let err = InstanceError::DatabaseAlreadyExists {
365 name: "existing-database".to_string(),
366 };
367 assert!(err.is_already_exists());
368 assert_eq!(err.database_name(), Some("existing-database"));
369
370 let err = InstanceError::InstanceAlreadyExists;
371 assert!(err.is_already_exists());
372
373 let err = InstanceError::EntryNotFound {
374 entry_id: ID::from_bytes("test-entry"),
375 };
376 assert!(err.is_not_found());
377 assert_eq!(err.entry_id(), Some(&ID::from_bytes("test-entry")));
378
379 let err = InstanceError::AuthenticationRequired;
380 assert!(err.is_authentication_error());
381
382 let err = InstanceError::TransactionAlreadyCommitted;
383 assert!(err.is_operation_error());
384
385 let err = InstanceError::InvalidDataType {
386 expected: "string".to_string(),
387 actual: "number".to_string(),
388 };
389 assert!(err.is_validation_error());
390
391 let err = InstanceError::DatabaseStateCorruption {
392 reason: "test".to_string(),
393 };
394 assert!(err.is_corruption_error());
395 }
396
397 #[test]
398 fn test_error_conversion() {
399 let base_err = InstanceError::DatabaseNotFound {
400 name: "test".to_string(),
401 };
402 let err: crate::Error = base_err.into();
403 match err {
404 crate::Error::Instance(e) => match *e {
405 InstanceError::DatabaseNotFound { name } => {
406 assert_eq!(name, "test")
407 }
408 _ => panic!("Unexpected error variant"),
409 },
410 _ => panic!("Unexpected error variant"),
411 }
412 }
413}