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

eidetica/transaction/
errors.rs

1//! Transaction specific errors
2//!
3//! This module contains error types specific to transactions, which compose
4//! errors from multiple modules into a cohesive error handling system for
5//! cross-module operations.
6
7use thiserror::Error;
8
9use crate::entry::ID;
10
11/// Errors that can occur during transactions
12///
13/// `TransactionError` represents failures specific to transactions that
14/// span multiple modules and require coordinated error handling. These errors
15/// typically occur during entry construction, commit operations, and cross-module
16/// data staging.
17#[non_exhaustive]
18#[derive(Debug, Error)]
19pub enum TransactionError {
20    /// Transaction has already been committed and cannot be used again
21    #[error("Transaction has already been committed")]
22    TransactionAlreadyCommitted,
23
24    /// Tips array cannot be empty when creating a transaction
25    #[error("Empty tips array not allowed for transaction")]
26    EmptyTipsNotAllowed,
27
28    /// Invalid tip provided to transaction
29    #[error("Invalid tip for transaction: {tip_id}")]
30    InvalidTip { tip_id: ID },
31
32    /// Entry construction failed during commit
33    #[error("Entry construction failed: {reason}")]
34    EntryConstructionFailed { reason: String },
35
36    /// Entry signing failed during commit
37    #[error("Entry signing failed for key '{key_name}': {reason}")]
38    EntrySigningFailed { key_name: String, reason: String },
39
40    /// Required signing key not found
41    #[error("Signing key not found: {key_name}")]
42    SigningKeyNotFound { key_name: String },
43
44    /// Authentication is required but not configured
45    #[error("Authentication required but not configured")]
46    AuthenticationRequired,
47
48    /// Authentication configuration is missing
49    #[error("No authentication configuration found")]
50    NoAuthConfiguration,
51
52    /// Authentication configuration exists but is corrupted or malformed
53    #[error("Authentication configuration is corrupted or malformed")]
54    CorruptedAuthConfiguration,
55
56    /// Insufficient permissions for the operation
57    #[error("Insufficient permissions for operation")]
58    InsufficientPermissions,
59
60    /// Entry signature verification failed
61    #[error("Entry signature verification failed")]
62    SignatureVerificationFailed,
63
64    /// Entry validation failed (signature, permissions, or configuration)
65    #[error("Entry validation failed")]
66    EntryValidationFailed,
67
68    /// Store data deserialization failed
69    #[error("Store data deserialization failed for '{store}': {reason}")]
70    StoreDeserializationFailed { store: String, reason: String },
71
72    /// Backend operation failed during commit
73    #[error("Backend operation failed during commit: {reason}")]
74    BackendOperationFailed { reason: String },
75
76    /// Attempt to open a system subtree (`_settings`, `_root`, `_index`, …) at
77    /// a moment when the transaction has locked them. Currently used by
78    /// `Database::create_with_init` to prevent its init callback from
79    /// clobbering the system subtrees that `create_with_init` itself manages.
80    #[error(
81        "System subtree '{name}' is locked for this transaction (init callbacks may not open `_`-prefixed subtrees directly)"
82    )]
83    SystemSubtreeLocked { name: String },
84}
85
86impl TransactionError {
87    /// Check if this error indicates the operation was already committed
88    pub fn is_already_committed(&self) -> bool {
89        matches!(self, TransactionError::TransactionAlreadyCommitted)
90    }
91
92    /// Check if this error is authentication-related
93    pub fn is_authentication_error(&self) -> bool {
94        matches!(
95            self,
96            TransactionError::SigningKeyNotFound { .. }
97                | TransactionError::AuthenticationRequired
98                | TransactionError::NoAuthConfiguration
99                | TransactionError::CorruptedAuthConfiguration
100                | TransactionError::InsufficientPermissions
101                | TransactionError::SignatureVerificationFailed
102                | TransactionError::EntrySigningFailed { .. }
103                | TransactionError::EntryValidationFailed
104        )
105    }
106
107    /// Check if this error is related to entry operations
108    pub fn is_entry_error(&self) -> bool {
109        matches!(
110            self,
111            TransactionError::EntryConstructionFailed { .. }
112                | TransactionError::EntrySigningFailed { .. }
113                | TransactionError::SignatureVerificationFailed
114                | TransactionError::EntryValidationFailed
115        )
116    }
117
118    /// Check if this error is related to store operations
119    pub fn is_store_error(&self) -> bool {
120        matches!(self, TransactionError::StoreDeserializationFailed { .. })
121    }
122
123    /// Check if this error is related to backend operations
124    pub fn is_backend_error(&self) -> bool {
125        matches!(self, TransactionError::BackendOperationFailed { .. })
126    }
127
128    /// Check if this error is related to validation
129    pub fn is_validation_error(&self) -> bool {
130        matches!(
131            self,
132            TransactionError::InvalidTip { .. } | TransactionError::EmptyTipsNotAllowed
133        )
134    }
135
136    /// Get the store name if this is a store-related error
137    pub fn store_name(&self) -> Option<&str> {
138        match self {
139            TransactionError::StoreDeserializationFailed { store, .. } => Some(store),
140            _ => None,
141        }
142    }
143
144    /// Get the key name if this is an authentication-related error
145    pub fn key_name(&self) -> Option<&str> {
146        match self {
147            TransactionError::SigningKeyNotFound { key_name }
148            | TransactionError::EntrySigningFailed { key_name, .. } => Some(key_name),
149            _ => None,
150        }
151    }
152}
153
154// Conversion from TransactionError to the main Error type
155impl From<TransactionError> for crate::Error {
156    fn from(err: TransactionError) -> Self {
157        crate::Error::Transaction(Box::new(err))
158    }
159}
160
161#[cfg(test)]
162mod tests {
163    use super::*;
164
165    #[test]
166    fn test_error_classification() {
167        // Test authentication errors
168        let auth_err = TransactionError::AuthenticationRequired;
169        assert!(auth_err.is_authentication_error());
170        assert!(!auth_err.is_entry_error());
171
172        // Test entry errors
173        let entry_err = TransactionError::EntryConstructionFailed {
174            reason: "test".to_owned(),
175        };
176        assert!(entry_err.is_entry_error());
177        assert!(!entry_err.is_authentication_error());
178
179        // Test store errors
180        let store_err = TransactionError::StoreDeserializationFailed {
181            store: "test_store".to_owned(),
182            reason: "test".to_owned(),
183        };
184        assert!(store_err.is_store_error());
185        assert_eq!(store_err.store_name(), Some("test_store"));
186
187        // Test validation errors
188        let validation_err = TransactionError::EmptyTipsNotAllowed;
189        assert!(validation_err.is_validation_error());
190        assert!(!validation_err.is_backend_error());
191    }
192
193    #[test]
194    fn test_already_committed() {
195        let err = TransactionError::TransactionAlreadyCommitted;
196        assert!(err.is_already_committed());
197    }
198
199    #[test]
200    fn test_key_name_extraction() {
201        let err = TransactionError::SigningKeyNotFound {
202            key_name: "test_key".to_owned(),
203        };
204        assert_eq!(err.key_name(), Some("test_key"));
205
206        let other_err = TransactionError::AuthenticationRequired;
207        assert_eq!(other_err.key_name(), None);
208    }
209}