use axum::{http::StatusCode, response::IntoResponse, Json}; use serde_json::json; #[derive(Debug)] pub enum ApiError { DatabaseError(sqlx::Error), AuthError(String), ValidationError(String), NotFound(String), BadRequest(String), Database(String), Internal(String), FileError(std::io::Error), JwtError(jsonwebtoken::errors::Error), BcryptError(bcrypt::BcryptError), SerdeError(serde_json::Error), // Enhanced specific error types for better internal handling // All map to existing HTTP responses - zero breaking changes BulletinNotFound(String), EventNotFound(String), ScheduleNotFound(String), MemberNotFound(String), HymnNotFound(String), UserNotFound(String), // Processing errors BulletinProcessingError(String), MediaProcessingError(String), EmailSendError(String), UploadError(String), // Configuration errors ConfigurationError(String), MissingConfiguration(String), // Business logic errors DuplicateEntry(String), InvalidDateRange(String), InvalidRecurringPattern(String), // External service errors OwncastConnectionError(String), ExternalServiceError { service: String, message: String }, } impl IntoResponse for ApiError { fn into_response(self) -> axum::response::Response { let (status, message) = match self { ApiError::DatabaseError(e) => { tracing::error!("Database error: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Database error".to_string()) } ApiError::AuthError(msg) => (StatusCode::UNAUTHORIZED, msg), ApiError::ValidationError(msg) => (StatusCode::BAD_REQUEST, msg), ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, msg), ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg), ApiError::Database(msg) => { tracing::error!("Database error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, msg) } ApiError::Internal(msg) => { tracing::error!("Internal error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, msg) } ApiError::FileError(e) => { tracing::error!("File error: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, "File operation failed".to_string()) } ApiError::JwtError(e) => { tracing::error!("JWT error: {:?}", e); (StatusCode::UNAUTHORIZED, "Invalid token".to_string()) } ApiError::BcryptError(e) => { tracing::error!("Bcrypt error: {:?}", e); (StatusCode::INTERNAL_SERVER_ERROR, "Password hashing error".to_string()) } ApiError::SerdeError(e) => { tracing::error!("Serde error: {:?}", e); (StatusCode::BAD_REQUEST, "Invalid JSON".to_string()) } // Enhanced error types - map to existing HTTP responses for zero breaking changes // All *NotFound variants map to 404 ApiError::BulletinNotFound(msg) | ApiError::EventNotFound(msg) | ApiError::ScheduleNotFound(msg) | ApiError::MemberNotFound(msg) | ApiError::HymnNotFound(msg) | ApiError::UserNotFound(msg) => { tracing::warn!("Resource not found: {}", msg); (StatusCode::NOT_FOUND, msg) } // Processing errors map to 500 ApiError::BulletinProcessingError(msg) | ApiError::MediaProcessingError(msg) => { tracing::error!("Processing error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, msg) } // Email and upload errors map to 500 ApiError::EmailSendError(msg) | ApiError::UploadError(msg) => { tracing::error!("Service error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, msg) } // Configuration errors map to 500 ApiError::ConfigurationError(msg) | ApiError::MissingConfiguration(msg) => { tracing::error!("Configuration error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, "Server configuration error".to_string()) } // Business logic errors map to 400 ApiError::DuplicateEntry(msg) | ApiError::InvalidDateRange(msg) | ApiError::InvalidRecurringPattern(msg) => { tracing::warn!("Business logic error: {}", msg); (StatusCode::BAD_REQUEST, msg) } // External service errors map to 500 ApiError::OwncastConnectionError(msg) => { tracing::error!("Owncast connection error: {}", msg); (StatusCode::INTERNAL_SERVER_ERROR, "External service unavailable".to_string()) } ApiError::ExternalServiceError { service, message } => { tracing::error!("External service '{}' error: {}", service, message); (StatusCode::INTERNAL_SERVER_ERROR, "External service error".to_string()) } }; ( status, Json(json!({ "success": false, "error": message })), ) .into_response() } } impl From for ApiError { fn from(error: sqlx::Error) -> Self { ApiError::DatabaseError(error) } } impl From for ApiError { fn from(error: std::io::Error) -> Self { ApiError::FileError(error) } } impl From for ApiError { fn from(error: jsonwebtoken::errors::Error) -> Self { ApiError::JwtError(error) } } impl From for ApiError { fn from(error: bcrypt::BcryptError) -> Self { ApiError::BcryptError(error) } } impl From for ApiError { fn from(error: serde_json::Error) -> Self { ApiError::SerdeError(error) } } impl ApiError { // Constructor methods for common patterns - makes code more readable and consistent pub fn bulletin_not_found(id: impl std::fmt::Display) -> Self { Self::BulletinNotFound(format!("Bulletin not found: {}", id)) } pub fn event_not_found(id: impl std::fmt::Display) -> Self { Self::EventNotFound(format!("Event not found: {}", id)) } pub fn schedule_not_found(date: impl std::fmt::Display) -> Self { Self::ScheduleNotFound(format!("Schedule not found for date: {}", date)) } pub fn hymn_not_found(hymnal: &str, number: i32) -> Self { Self::HymnNotFound(format!("Hymn {} not found in {}", number, hymnal)) } pub fn user_not_found(identifier: impl std::fmt::Display) -> Self { Self::UserNotFound(format!("User not found: {}", identifier)) } pub fn member_not_found(id: impl std::fmt::Display) -> Self { Self::MemberNotFound(format!("Member not found: {}", id)) } pub fn bulletin_processing_failed(reason: impl std::fmt::Display) -> Self { Self::BulletinProcessingError(format!("Bulletin processing failed: {}", reason)) } pub fn media_processing_failed(reason: impl std::fmt::Display) -> Self { Self::MediaProcessingError(format!("Media processing failed: {}", reason)) } pub fn email_send_failed(reason: impl std::fmt::Display) -> Self { Self::EmailSendError(format!("Email sending failed: {}", reason)) } pub fn upload_failed(reason: impl std::fmt::Display) -> Self { Self::UploadError(format!("Upload failed: {}", reason)) } pub fn invalid_date_range(start: impl std::fmt::Display, end: impl std::fmt::Display) -> Self { Self::InvalidDateRange(format!("Invalid date range: {} to {}", start, end)) } pub fn duplicate_entry(resource: &str, identifier: impl std::fmt::Display) -> Self { Self::DuplicateEntry(format!("{} already exists: {}", resource, identifier)) } pub fn missing_config(key: &str) -> Self { Self::MissingConfiguration(format!("Missing required configuration: {}", key)) } pub fn external_service_failed(service: &str, message: impl std::fmt::Display) -> Self { Self::ExternalServiceError { service: service.to_string(), message: message.to_string(), } } } pub type Result = std::result::Result;