use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde_json::json;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum AppError {
#[error("Authentication error: {0}")]
Auth(#[from] AuthError),
#[error("File system error: {0}")]
FileSystem(#[from] FileSystemError),
#[error("Search error: {0}")]
Search(#[from] SearchError),
#[error("AI service error: {0}")]
Ai(#[from] AiError),
#[error("Configuration error: {0}")]
Config(#[from] ConfigError),
#[error("Validation error: {0}")]
Validation(#[from] ValidationError),
#[error("Internal server error: {0}")]
Internal(#[from] anyhow::Error),
}
#[derive(Error, Debug)]
pub enum AuthError {
#[error("Invalid credentials")]
InvalidCredentials,
#[error("Token expired")]
TokenExpired,
#[error("Invalid token")]
InvalidToken,
#[error("Insufficient permissions")]
InsufficientPermissions,
#[error("User not found")]
UserNotFound,
#[error("Registration disabled")]
RegistrationDisabled,
}
#[derive(Error, Debug)]
pub enum FileSystemError {
#[error("File not found: {path}")]
FileNotFound {
path: String,
},
#[error("Directory not found: {path}")]
DirectoryNotFound {
path: String,
},
#[error("Permission denied: {path}")]
PermissionDenied {
path: String,
},
#[error("Path outside allowed directory: {path}")]
PathTraversal {
path: String,
},
#[error("File too large: {size} bytes (max: {max_size} bytes)")]
FileTooLarge {
size: u64,
max_size: u64,
},
#[error("Disk full")]
DiskFull,
#[error("Invalid file name: {name}")]
InvalidFileName {
name: String,
},
}
#[derive(Error, Debug)]
pub enum SearchError {
#[error("Index not found")]
IndexNotFound,
#[error("Invalid query: {query}")]
InvalidQuery {
query: String,
},
#[error("Index corruption detected")]
IndexCorruption,
#[error("Search timeout")]
Timeout,
}
#[derive(Error, Debug)]
pub enum AiError {
#[error("AI service not available")]
ServiceUnavailable,
#[error("API key not configured")]
ApiKeyNotConfigured,
#[error("Rate limit exceeded")]
RateLimitExceeded,
#[error("Invalid model: {model}")]
InvalidModel {
model: String,
},
#[error("Content too long: {length} tokens (max: {max_length})")]
ContentTooLong {
length: usize,
max_length: usize,
},
}
#[derive(Error, Debug)]
pub enum ConfigError {
#[error("Missing required configuration: {key}")]
MissingRequired {
key: String,
},
#[error("Invalid configuration value for {key}: {value}")]
InvalidValue {
key: String,
value: String,
},
#[error("Configuration file not found: {path}")]
FileNotFound {
path: String,
},
#[error("Configuration parse error: {message}")]
ParseError {
message: String,
},
}
#[derive(Error, Debug)]
pub enum ValidationError {
#[error("Invalid email format: {email}")]
InvalidEmail {
email: String,
},
#[error("Password too short (minimum {min_length} characters)")]
PasswordTooShort {
min_length: usize,
},
#[error("Invalid file extension: {extension}")]
InvalidFileExtension {
extension: String,
},
#[error("Field required: {field}")]
FieldRequired {
field: String,
},
#[error("Value out of range for {field}: {value} (min: {min}, max: {max})")]
ValueOutOfRange {
field: String,
value: String,
min: String,
max: String,
},
}
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (status, error_message) = match &self {
AppError::Auth(auth_error) => match auth_error {
AuthError::InvalidCredentials => (StatusCode::UNAUTHORIZED, "Invalid credentials"),
AuthError::TokenExpired => (StatusCode::UNAUTHORIZED, "Token expired"),
AuthError::InvalidToken => (StatusCode::UNAUTHORIZED, "Invalid token"),
AuthError::InsufficientPermissions => (StatusCode::FORBIDDEN, "Insufficient permissions"),
AuthError::UserNotFound => (StatusCode::NOT_FOUND, "User not found"),
AuthError::RegistrationDisabled => (StatusCode::FORBIDDEN, "Registration disabled"),
},
AppError::FileSystem(fs_error) => match fs_error {
FileSystemError::FileNotFound { .. } => (StatusCode::NOT_FOUND, "File not found"),
FileSystemError::DirectoryNotFound { .. } => (StatusCode::NOT_FOUND, "Directory not found"),
FileSystemError::PermissionDenied { .. } => (StatusCode::FORBIDDEN, "Permission denied"),
FileSystemError::PathTraversal { .. } => (StatusCode::BAD_REQUEST, "Invalid path"),
FileSystemError::FileTooLarge { .. } => (StatusCode::PAYLOAD_TOO_LARGE, "File too large"),
FileSystemError::DiskFull => (StatusCode::INSUFFICIENT_STORAGE, "Disk full"),
FileSystemError::InvalidFileName { .. } => (StatusCode::BAD_REQUEST, "Invalid file name"),
},
AppError::Search(search_error) => match search_error {
SearchError::IndexNotFound => (StatusCode::SERVICE_UNAVAILABLE, "Search index not available"),
SearchError::InvalidQuery { .. } => (StatusCode::BAD_REQUEST, "Invalid search query"),
SearchError::IndexCorruption => (StatusCode::INTERNAL_SERVER_ERROR, "Search index corrupted"),
SearchError::Timeout => (StatusCode::REQUEST_TIMEOUT, "Search timeout"),
},
AppError::Ai(ai_error) => match ai_error {
AiError::ServiceUnavailable => (StatusCode::SERVICE_UNAVAILABLE, "AI service unavailable"),
AiError::ApiKeyNotConfigured => (StatusCode::SERVICE_UNAVAILABLE, "AI service not configured"),
AiError::RateLimitExceeded => (StatusCode::TOO_MANY_REQUESTS, "Rate limit exceeded"),
AiError::InvalidModel { .. } => (StatusCode::BAD_REQUEST, "Invalid AI model"),
AiError::ContentTooLong { .. } => (StatusCode::PAYLOAD_TOO_LARGE, "Content too long"),
},
AppError::Config(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Configuration error"),
AppError::Validation(_) => (StatusCode::BAD_REQUEST, "Validation error"),
AppError::Internal(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Internal server error"),
};
let body = Json(json!({
"success": false,
"error": error_message,
"details": self.to_string(),
}));
(status, body).into_response()
}
}
pub type Result<T> = std::result::Result<T, AppError>;
#[macro_export]
macro_rules! auth_error {
($variant:ident) => {
$crate::error::AppError::Auth($crate::error::AuthError::$variant)
};
($variant:ident, $($arg:expr),*) => {
$crate::error::AppError::Auth($crate::error::AuthError::$variant { $($arg),* })
};
}
#[macro_export]
macro_rules! fs_error {
($variant:ident, $($arg:expr),*) => {
$crate::error::AppError::FileSystem($crate::error::FileSystemError::$variant { $($arg),* })
};
}
#[macro_export]
macro_rules! validation_error {
($variant:ident, $($arg:expr),*) => {
$crate::error::AppError::Validation($crate::error::ValidationError::$variant { $($arg),* })
};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let error = AppError::Auth(AuthError::InvalidCredentials);
assert_eq!(error.to_string(), "Authentication error: Invalid credentials");
}
#[test]
fn test_file_system_error() {
let error = AppError::FileSystem(FileSystemError::FileNotFound {
path: "/test/file.txt".to_string(),
});
assert!(error.to_string().contains("File not found: /test/file.txt"));
}
#[test]
fn test_validation_error() {
let error = AppError::Validation(ValidationError::InvalidEmail {
email: "invalid-email".to_string(),
});
assert!(error.to_string().contains("Invalid email format"));
}
}