use thiserror::Error;
use crate::entry::ID;
#[non_exhaustive]
#[derive(Debug, Error)]
pub enum InstanceError {
#[error("Database not found: {name}")]
DatabaseNotFound {
name: String,
},
#[error("Database already exists: {name}")]
DatabaseAlreadyExists {
name: String,
},
#[error("Instance already exists on backend (found device key and system databases)")]
InstanceAlreadyExists,
#[error("Entry '{entry_id}' does not belong to database '{database_id}'")]
EntryNotInDatabase {
entry_id: ID,
database_id: ID,
},
#[error("Entry not found: {entry_id}")]
EntryNotFound {
entry_id: ID,
},
#[error("Transaction has already been committed")]
TransactionAlreadyCommitted,
#[error("Cannot create transaction with empty tips")]
EmptyTipsNotAllowed,
#[error("Tip entry '{tip_id}' does not belong to database '{database_id}'")]
InvalidTip {
tip_id: ID,
database_id: ID,
},
#[error("Signing key '{key_name}' not found in backend")]
SigningKeyNotFound {
key_name: String,
},
#[error("Authentication required but no key configured")]
AuthenticationRequired,
#[error("Device key (_device_key) not found in backend")]
DeviceKeyNotFound,
#[error("No authentication configuration found")]
NoAuthConfiguration,
#[error("Authentication validation failed: {reason}")]
AuthenticationValidationFailed {
reason: String,
},
#[error("Insufficient permissions for operation")]
InsufficientPermissions,
#[error("Signature verification failed")]
SignatureVerificationFailed,
#[error("Invalid data type: expected {expected}, got {actual}")]
InvalidDataType {
expected: String,
actual: String,
},
#[error("Serialization failed for {context}")]
SerializationFailed {
context: String,
},
#[error("Invalid database configuration: {reason}")]
InvalidDatabaseConfiguration {
reason: String,
},
#[error("Settings validation failed: {reason}")]
SettingsValidationFailed {
reason: String,
},
#[error("Invalid operation: {reason}")]
InvalidOperation {
reason: String,
},
#[error("Database initialization failed: {reason}")]
DatabaseInitializationFailed {
reason: String,
},
#[error("Entry validation failed: {reason}")]
EntryValidationFailed {
reason: String,
},
#[error("Database state corruption detected: {reason}")]
DatabaseStateCorruption {
reason: String,
},
#[error("Operation not supported: {operation}")]
OperationNotSupported {
operation: String,
},
#[error("Instance has been dropped")]
InstanceDropped,
#[error("Sync has already been enabled on this Instance")]
SyncAlreadyEnabled,
#[error("System database not found: {database_name}")]
SystemDatabaseNotFound {
database_name: String,
},
}
impl InstanceError {
pub fn is_not_found(&self) -> bool {
matches!(
self,
InstanceError::DatabaseNotFound { .. }
| InstanceError::EntryNotFound { .. }
| InstanceError::SigningKeyNotFound { .. }
| InstanceError::SystemDatabaseNotFound { .. }
)
}
pub fn is_already_exists(&self) -> bool {
matches!(
self,
InstanceError::DatabaseAlreadyExists { .. } | InstanceError::InstanceAlreadyExists
)
}
pub fn is_authentication_error(&self) -> bool {
matches!(
self,
InstanceError::AuthenticationRequired
| InstanceError::NoAuthConfiguration
| InstanceError::AuthenticationValidationFailed { .. }
| InstanceError::InsufficientPermissions
| InstanceError::SignatureVerificationFailed
| InstanceError::SigningKeyNotFound { .. }
)
}
pub fn is_operation_error(&self) -> bool {
matches!(
self,
InstanceError::TransactionAlreadyCommitted
| InstanceError::EmptyTipsNotAllowed
| InstanceError::InvalidOperation { .. }
)
}
pub fn is_validation_error(&self) -> bool {
matches!(
self,
InstanceError::EntryNotInDatabase { .. }
| InstanceError::InvalidTip { .. }
| InstanceError::InvalidDataType { .. }
| InstanceError::InvalidDatabaseConfiguration { .. }
| InstanceError::SettingsValidationFailed { .. }
| InstanceError::EntryValidationFailed { .. }
)
}
pub fn is_corruption_error(&self) -> bool {
matches!(self, InstanceError::DatabaseStateCorruption { .. })
}
pub fn entry_id(&self) -> Option<&ID> {
match self {
InstanceError::EntryNotFound { entry_id }
| InstanceError::EntryNotInDatabase { entry_id, .. }
| InstanceError::InvalidTip {
tip_id: entry_id, ..
} => Some(entry_id),
_ => None,
}
}
pub fn database_id(&self) -> Option<&ID> {
match self {
InstanceError::EntryNotInDatabase { database_id, .. }
| InstanceError::InvalidTip { database_id, .. } => Some(database_id),
_ => None,
}
}
pub fn database_name(&self) -> Option<&str> {
match self {
InstanceError::DatabaseNotFound { name }
| InstanceError::DatabaseAlreadyExists { name } => Some(name),
_ => None,
}
}
}
impl From<InstanceError> for crate::Error {
fn from(err: InstanceError) -> Self {
crate::Error::Instance(err)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_helpers() {
let err = InstanceError::DatabaseNotFound {
name: "test-database".to_string(),
};
assert!(err.is_not_found());
assert_eq!(err.database_name(), Some("test-database"));
let err = InstanceError::DatabaseAlreadyExists {
name: "existing-database".to_string(),
};
assert!(err.is_already_exists());
assert_eq!(err.database_name(), Some("existing-database"));
let err = InstanceError::InstanceAlreadyExists;
assert!(err.is_already_exists());
let err = InstanceError::EntryNotFound {
entry_id: ID::from("test-entry"),
};
assert!(err.is_not_found());
assert_eq!(err.entry_id(), Some(&ID::from("test-entry")));
let err = InstanceError::AuthenticationRequired;
assert!(err.is_authentication_error());
let err = InstanceError::TransactionAlreadyCommitted;
assert!(err.is_operation_error());
let err = InstanceError::InvalidDataType {
expected: "string".to_string(),
actual: "number".to_string(),
};
assert!(err.is_validation_error());
let err = InstanceError::DatabaseStateCorruption {
reason: "test".to_string(),
};
assert!(err.is_corruption_error());
}
#[test]
fn test_error_conversion() {
let base_err = InstanceError::DatabaseNotFound {
name: "test".to_string(),
};
let err: crate::Error = base_err.into();
match err {
crate::Error::Instance(InstanceError::DatabaseNotFound { name }) => {
assert_eq!(name, "test")
}
_ => panic!("Unexpected error variant"),
}
}
}