Development Documentation (main branch) - For stable release docs, see docs.rs/eidetica
Skip to main content

eidetica/instance/
errors.rs

1//! Base database error types for the Eidetica library.
2//!
3//! This module defines structured error types for tree operations, entry management,
4//! and database operations, providing better error context and type safety compared to string-based errors.
5
6use std::path::PathBuf;
7
8use thiserror::Error;
9
10use crate::entry::ID;
11
12/// Errors that can occur during base database operations.
13///
14/// # Stability
15///
16/// - New variants may be added in minor versions (enum is `#[non_exhaustive]`)
17/// - Existing variants will not be removed in minor versions
18/// - Field additions/changes require a major version bump
19/// - Helper methods like `is_*()` provide stable APIs
20#[non_exhaustive]
21#[derive(Debug, Error)]
22pub enum InstanceError {
23    /// Database not found by name.
24    #[error("Database not found: {name}")]
25    DatabaseNotFound {
26        /// The name of the database that was not found
27        name: String,
28    },
29
30    /// Database already exists with the given name.
31    #[error("Database already exists: {name}")]
32    DatabaseAlreadyExists {
33        /// The name of the database that already exists
34        name: String,
35    },
36
37    /// Instance already exists on this backend.
38    #[error("Instance already exists on backend (found device key and system databases)")]
39    InstanceAlreadyExists,
40
41    /// Backend has no Instance on it; the operator must create one before
42    /// it can be opened.
43    ///
44    /// Returned by [`Instance::connect`](super::Instance::connect) when the
45    /// backend at the URL has no instance metadata. Use
46    /// [`Instance::connect_or_create`](super::Instance::connect_or_create)
47    /// with a [`NewUser`](super::NewUser) to initialise it first.
48    #[error(
49        "Instance not initialised on this backend; use Instance::connect_or_create to bootstrap"
50    )]
51    NotInitialized,
52
53    /// URL failed to parse or is missing required components.
54    #[error("Invalid URL `{url}`: {reason}")]
55    InvalidUrl {
56        /// The URL string the caller provided.
57        url: String,
58        /// What was wrong, with a hint where possible.
59        reason: String,
60    },
61
62    /// Scheme is not recognised (e.g. `mysql://`, `foo://`).
63    #[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        /// The scheme the caller passed.
69        scheme: String,
70        /// Best-effort typo suggestion, if any.
71        suggested: Option<&'static str>,
72    },
73
74    /// Scheme is recognised but its backend isn't compiled into this build.
75    #[error(
76        "Backend for `{scheme}://` is not available in this build; rebuild with `--features {missing_feature}`"
77    )]
78    BackendUnavailable {
79        /// The scheme that requires the missing feature.
80        scheme: &'static str,
81        /// Cargo feature flag that enables the backend.
82        missing_feature: &'static str,
83    },
84
85    /// Snapshot file is present but cannot be loaded.
86    #[error("Invalid snapshot at `{path}`: {reason}")]
87    InvalidSnapshot {
88        /// Path to the offending snapshot file.
89        path: PathBuf,
90        /// Why the load failed (parse error, missing metadata, etc.).
91        reason: String,
92    },
93
94    /// Snapshotting is only supported on the in-memory backend.
95    #[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    /// Entry does not belong to the specified database.
101    #[error("Entry '{entry_id}' does not belong to database '{database_id}'")]
102    EntryNotInDatabase {
103        /// The ID of the entry
104        entry_id: ID,
105        /// The ID of the database
106        database_id: ID,
107    },
108
109    /// Entry not found by ID.
110    #[error("Entry not found: {entry_id}")]
111    EntryNotFound {
112        /// The ID of the entry that was not found
113        entry_id: ID,
114    },
115
116    /// Transaction has already been committed and cannot be modified.
117    #[error("Transaction has already been committed")]
118    TransactionAlreadyCommitted,
119
120    /// Cannot create transaction with empty tips.
121    #[error("Cannot create transaction with empty tips")]
122    EmptyTipsNotAllowed,
123
124    /// Tip entry does not belong to the specified database.
125    #[error("Tip entry '{tip_id}' does not belong to database '{database_id}'")]
126    InvalidTip {
127        /// The ID of the invalid tip entry
128        tip_id: ID,
129        /// The ID of the database
130        database_id: ID,
131    },
132
133    /// Signing key not found in backend storage.
134    #[error("Signing key '{key_name}' not found in backend")]
135    SigningKeyNotFound {
136        /// The name of the signing key that was not found
137        key_name: String,
138    },
139
140    /// Authentication is required but no key is configured.
141    #[error("Authentication required but no key configured")]
142    AuthenticationRequired,
143
144    /// Device key not found in instance metadata.
145    #[error("Device key not found in instance metadata")]
146    DeviceKeyNotFound,
147
148    /// Device key in secrets does not match the public key in metadata.
149    #[error("Device key in secrets does not match public key in metadata")]
150    DeviceKeyMismatch,
151
152    /// No authentication configuration found.
153    #[error("No authentication configuration found")]
154    NoAuthConfiguration,
155
156    /// Authentication validation failed.
157    #[error("Authentication validation failed: {reason}")]
158    AuthenticationValidationFailed {
159        /// Description of why authentication validation failed
160        reason: String,
161    },
162
163    /// Insufficient permissions for the requested operation.
164    #[error("Insufficient permissions for operation")]
165    InsufficientPermissions,
166
167    /// Signature verification failed.
168    #[error("Signature verification failed")]
169    SignatureVerificationFailed,
170
171    /// Invalid data type encountered.
172    #[error("Invalid data type: expected {expected}, got {actual}")]
173    InvalidDataType {
174        /// The expected data type
175        expected: String,
176        /// The actual data type found
177        actual: String,
178    },
179
180    /// Serialization failed.
181    #[error("Serialization failed for {context}")]
182    SerializationFailed {
183        /// The context where serialization failed
184        context: String,
185    },
186
187    /// Invalid database configuration.
188    #[error("Invalid database configuration: {reason}")]
189    InvalidDatabaseConfiguration {
190        /// Description of why the database configuration is invalid
191        reason: String,
192    },
193
194    /// Settings validation failed.
195    #[error("Settings validation failed: {reason}")]
196    SettingsValidationFailed {
197        /// Description of why settings validation failed
198        reason: String,
199    },
200
201    /// Invalid operation attempted.
202    #[error("Invalid operation: {reason}")]
203    InvalidOperation {
204        /// Description of why the operation is invalid
205        reason: String,
206    },
207
208    /// Database initialization failed.
209    #[error("Database initialization failed: {reason}")]
210    DatabaseInitializationFailed {
211        /// Description of why database initialization failed
212        reason: String,
213    },
214
215    /// Entry validation failed.
216    #[error("Entry validation failed: {reason}")]
217    EntryValidationFailed {
218        /// Description of why entry validation failed
219        reason: String,
220    },
221
222    /// Database state is corrupted or inconsistent.
223    #[error("Database state corruption detected: {reason}")]
224    DatabaseStateCorruption {
225        /// Description of the corruption detected
226        reason: String,
227    },
228
229    /// Operation is not supported in the current mode or not yet implemented.
230    #[error("Operation not supported: {operation}")]
231    OperationNotSupported {
232        /// Description of the unsupported operation
233        operation: String,
234    },
235
236    /// Instance has been dropped and is no longer available.
237    #[error("Instance has been dropped")]
238    InstanceDropped,
239
240    /// Sync has already been enabled on this Instance.
241    #[error("Sync has already been enabled on this Instance")]
242    SyncAlreadyEnabled,
243
244    /// System database not found during instance initialization.
245    #[error("System database not found: {database_name}")]
246    SystemDatabaseNotFound {
247        /// Name of the system database that was not found
248        database_name: String,
249    },
250}
251
252impl InstanceError {
253    /// Check if this error indicates a resource was not found.
254    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    /// Check if this error indicates a resource already exists.
265    pub fn is_already_exists(&self) -> bool {
266        matches!(
267            self,
268            InstanceError::DatabaseAlreadyExists { .. } | InstanceError::InstanceAlreadyExists
269        )
270    }
271
272    /// Check if this error is authentication-related.
273    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    /// Check if this error is operation-related.
286    pub fn is_operation_error(&self) -> bool {
287        matches!(
288            self,
289            InstanceError::TransactionAlreadyCommitted
290                | InstanceError::EmptyTipsNotAllowed
291                | InstanceError::InvalidOperation { .. }
292        )
293    }
294
295    /// Check if this error is validation-related.
296    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    /// Check if this error indicates corruption or inconsistency.
309    pub fn is_corruption_error(&self) -> bool {
310        matches!(self, InstanceError::DatabaseStateCorruption { .. })
311    }
312
313    /// Get the entry ID if this error is about a specific entry.
314    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    /// Get the database ID if this error is about a specific database.
326    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    /// Get the database name if this error is about a named database.
335    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
344// Conversion from BaseError to the main Error type
345impl From<InstanceError> for crate::Error {
346    fn from(err: InstanceError) -> Self {
347        // Use the new structured Base variant
348        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}