use axum::{
http::StatusCode,
response::{IntoResponse, Json, Response},
};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum DbError {
#[error("Collection '{0}' not found")]
CollectionNotFound(String),
#[error("Document with key '{0}' not found")]
DocumentNotFound(String),
#[error("Collection '{0}' already exists")]
CollectionAlreadyExists(String),
#[error("Invalid document: {0}")]
InvalidDocument(String),
#[error("Conflict: {0}")]
ConflictError(String),
#[error("Parse error: {0}")]
ParseError(String),
#[error("Query execution error: {0}")]
ExecutionError(String),
#[error("Bad Request: {0}")]
BadRequest(String),
#[error("{0}")]
RateLimited(String, u64),
#[error("Operation not supported: {0}")]
OperationNotSupported(String),
#[error("IO error: {0}")]
IoError(#[from] std::io::Error),
#[error("JSON error: {0}")]
JsonError(#[from] serde_json::Error),
#[error("Internal error: {0}")]
InternalError(String),
#[error("Operation timed out: {0}")]
Timeout(String),
#[error("Network error: {0}")]
NetworkError(String),
#[error("Transaction '{0}' not found")]
TransactionNotFound(String),
#[error("Transaction conflict: {0}")]
TransactionConflict(String),
#[error("Deadlock detected: {0}")]
DeadlockDetected(String),
#[error("Transaction timeout: {0}")]
TransactionTimeout(String),
#[error("Isolation violation: {0}")]
IsolationViolation(String),
#[error("Schema validation failed: {0}")]
SchemaValidationError(String),
#[error("Schema compilation failed: {0}")]
SchemaCompilationError(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Role not found: {0}")]
RoleNotFound(String),
}
pub type DbResult<T> = Result<T, DbError>;
impl serde::Serialize for DbError {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
serializer.collect_str(self)
}
}
impl From<rust_rocksdb::Error> for DbError {
fn from(err: rust_rocksdb::Error) -> Self {
DbError::InternalError(err.into())
}
}
impl IntoResponse for DbError {
fn into_response(self) -> Response {
let (status, message) = match &self {
DbError::CollectionNotFound(msg)
| DbError::DocumentNotFound(msg)
| DbError::TransactionNotFound(msg)
| DbError::RoleNotFound(msg) => (StatusCode::NOT_FOUND, msg.clone()),
DbError::CollectionAlreadyExists(msg)
| DbError::ConflictError(msg)
| DbError::TransactionConflict(msg) => (StatusCode::CONFLICT, msg.clone()),
DbError::InvalidDocument(msg)
| DbError::ParseError(msg)
| DbError::BadRequest(msg)
| DbError::SchemaValidationError(msg) => (StatusCode::BAD_REQUEST, msg.clone()),
DbError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, msg.clone()),
DbError::Forbidden(msg) => (StatusCode::FORBIDDEN, msg.clone()),
DbError::RateLimited(msg, _) => (StatusCode::TOO_MANY_REQUESTS, msg.clone()),
DbError::OperationNotSupported(msg) => (StatusCode::NOT_IMPLEMENTED, msg.clone()),
DbError::TransactionTimeout(_) => (StatusCode::REQUEST_TIMEOUT, self.to_string()),
DbError::Timeout(_) => (StatusCode::GATEWAY_TIMEOUT, self.to_string()),
_ => (StatusCode::INTERNAL_SERVER_ERROR, self.to_string()),
};
let body = serde_json::json!({
"error": message,
"code": status.as_u16(),
"type": format!("{:?}", self).split('(').next().unwrap_or("Error") });
let mut response = (status, Json(body)).into_response();
if let DbError::RateLimited(_, retry_after_secs) = &self {
if let Ok(value) = axum::http::HeaderValue::from_str(&retry_after_secs.to_string()) {
response
.headers_mut()
.insert(axum::http::header::RETRY_AFTER, value);
}
}
response
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_messages() {
let err = DbError::CollectionNotFound("users".to_string());
assert_eq!(err.to_string(), "Collection 'users' not found");
let err = DbError::DocumentNotFound("doc123".to_string());
assert_eq!(err.to_string(), "Document with key 'doc123' not found");
let err = DbError::CollectionAlreadyExists("users".to_string());
assert_eq!(err.to_string(), "Collection 'users' already exists");
let err = DbError::InvalidDocument("missing _key".to_string());
assert_eq!(err.to_string(), "Invalid document: missing _key");
let err = DbError::ParseError("unexpected token".to_string());
assert_eq!(err.to_string(), "Parse error: unexpected token");
let err = DbError::ExecutionError("division by zero".to_string());
assert_eq!(err.to_string(), "Query execution error: division by zero");
let err = DbError::BadRequest("invalid parameter".to_string());
assert_eq!(err.to_string(), "Bad Request: invalid parameter");
let err = DbError::OperationNotSupported("bulk delete".to_string());
assert_eq!(err.to_string(), "Operation not supported: bulk delete");
let err = DbError::InternalError("storage failure".to_string());
assert_eq!(err.to_string(), "Internal error: storage failure");
let err = DbError::NetworkError("connection refused".to_string());
assert_eq!(err.to_string(), "Network error: connection refused");
}
#[test]
fn test_transaction_errors() {
let err = DbError::TransactionNotFound("tx123".to_string());
assert_eq!(err.to_string(), "Transaction 'tx123' not found");
let err = DbError::TransactionConflict("write-write conflict".to_string());
assert_eq!(
err.to_string(),
"Transaction conflict: write-write conflict"
);
let err = DbError::DeadlockDetected("cycle detected".to_string());
assert_eq!(err.to_string(), "Deadlock detected: cycle detected");
let err = DbError::TransactionTimeout("exceeded 30s".to_string());
assert_eq!(err.to_string(), "Transaction timeout: exceeded 30s");
let err = DbError::IsolationViolation("phantom read".to_string());
assert_eq!(err.to_string(), "Isolation violation: phantom read");
}
#[test]
fn test_error_debug() {
let err = DbError::CollectionNotFound("test".to_string());
let debug = format!("{:?}", err);
assert!(debug.contains("CollectionNotFound"));
}
#[test]
fn test_db_result_type() {
let ok_result: DbResult<i32> = Ok(42);
assert!(matches!(ok_result, Ok(42)));
let err_result: DbResult<i32> = Err(DbError::InternalError("test".to_string()));
assert!(err_result.is_err());
}
}