use std::fmt::Debug;
pub trait ModuleError: std::error::Error + Send + Sync + Debug {
fn error_code(&self) -> &str;
fn status_code(&self) -> u16;
fn user_message(&self) -> &str;
fn is_retriable(&self) -> bool {
false
}
fn is_loggable(&self) -> bool {
self.status_code() >= 500
}
fn category(&self) -> ErrorCategory {
match self.status_code() {
400..=499 => ErrorCategory::ClientError,
500..=599 => ErrorCategory::ServerError,
_ => ErrorCategory::Unknown,
}
}
fn debug_context(&self) -> Option<String> {
None
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
ClientError,
ServerError,
ValidationError,
AuthError,
DatabaseError,
ExternalServiceError,
Unknown,
}
impl std::fmt::Display for ErrorCategory {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ClientError => write!(f, "client_error"),
Self::ServerError => write!(f, "server_error"),
Self::ValidationError => write!(f, "validation_error"),
Self::AuthError => write!(f, "auth_error"),
Self::DatabaseError => write!(f, "database_error"),
Self::ExternalServiceError => write!(f, "external_service_error"),
Self::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, serde::Serialize)]
pub struct ErrorResponse {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
impl ErrorResponse {
pub fn from_error<E: ModuleError>(error: &E) -> Self {
Self {
code: error.error_code().to_string(),
message: error.user_message().to_string(),
details: None,
}
}
pub fn with_details<E: ModuleError>(error: &E, details: serde_json::Value) -> Self {
Self {
code: error.error_code().to_string(),
message: error.user_message().to_string(),
details: Some(details),
}
}
}
#[derive(Debug, thiserror::Error)]
pub enum CommonError {
#[error("Entity not found: {entity_type} with id {id}")]
NotFound {
entity_type: &'static str,
id: String,
},
#[error("Entity already exists: {entity_type}")]
AlreadyExists { entity_type: &'static str },
#[error("Validation failed: {message}")]
ValidationFailed { message: String },
#[error("Unauthorized")]
Unauthorized,
#[error("Forbidden: {message}")]
Forbidden { message: String },
#[error("Internal error: {message}")]
Internal { message: String },
#[error("Conflict: {message}")]
Conflict { message: String },
}
impl ModuleError for CommonError {
fn error_code(&self) -> &str {
match self {
Self::NotFound { .. } => "NOT_FOUND",
Self::AlreadyExists { .. } => "ALREADY_EXISTS",
Self::ValidationFailed { .. } => "VALIDATION_FAILED",
Self::Unauthorized => "UNAUTHORIZED",
Self::Forbidden { .. } => "FORBIDDEN",
Self::Internal { .. } => "INTERNAL_ERROR",
Self::Conflict { .. } => "CONFLICT",
}
}
fn status_code(&self) -> u16 {
match self {
Self::NotFound { .. } => 404,
Self::AlreadyExists { .. } => 409,
Self::ValidationFailed { .. } => 400,
Self::Unauthorized => 401,
Self::Forbidden { .. } => 403,
Self::Internal { .. } => 500,
Self::Conflict { .. } => 409,
}
}
fn user_message(&self) -> &str {
match self {
Self::NotFound { .. } => "The requested resource was not found",
Self::AlreadyExists { .. } => "A resource with this identifier already exists",
Self::ValidationFailed { message } => message,
Self::Unauthorized => "Authentication required",
Self::Forbidden { message } => message,
Self::Internal { .. } => "An internal error occurred",
Self::Conflict { message } => message,
}
}
fn is_retriable(&self) -> bool {
matches!(self, Self::Internal { .. })
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, thiserror::Error)]
#[error("Test error")]
struct TestError;
impl ModuleError for TestError {
fn error_code(&self) -> &str {
"TEST_ERROR"
}
fn status_code(&self) -> u16 {
400
}
fn user_message(&self) -> &str {
"This is a test error"
}
}
#[test]
fn test_module_error_trait() {
let error = TestError;
assert_eq!(error.error_code(), "TEST_ERROR");
assert_eq!(error.status_code(), 400);
assert_eq!(error.user_message(), "This is a test error");
assert!(!error.is_retriable());
assert!(!error.is_loggable());
assert_eq!(error.category(), ErrorCategory::ClientError);
}
#[test]
fn test_error_response() {
let error = TestError;
let response = ErrorResponse::from_error(&error);
assert_eq!(response.code, "TEST_ERROR");
assert_eq!(response.message, "This is a test error");
}
#[test]
fn test_common_error() {
let error = CommonError::NotFound {
entity_type: "User",
id: "123".to_string(),
};
assert_eq!(error.error_code(), "NOT_FOUND");
assert_eq!(error.status_code(), 404);
}
}