use thiserror::Error;
use actix_web::HttpResponse;
use serde::Serialize;
use std::borrow::Cow;
use crate::validation::validators::ValidationError;
pub type Result<T> = std::result::Result<T, AppError>;
#[derive(Debug, Serialize)]
struct ErrorBody {
error: ErrorDetail,
}
#[derive(Debug, Serialize)]
struct ErrorDetail {
code: String,
message: String,
#[serde(skip_serializing_if = "Option::is_none")]
request_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
details: Option<Vec<FieldError>>,
}
#[derive(Debug, Serialize)]
struct FieldError {
field: String,
message: String,
}
#[derive(Debug, Error, serde::Serialize, serde::Deserialize)]
pub enum AppError {
#[error("Not found: {0}")]
NotFound(Cow<'static, str>),
#[error("Validation failed: {0}")]
Validation(Cow<'static, str>),
#[error("Validation failed")]
ValidationReport(Vec<ValidationError>),
#[error("Unauthorized: {0}")]
Unauthorized(Cow<'static, str>),
#[error("Forbidden")]
Forbidden,
#[error("Conflict: {0}")]
Conflict(Cow<'static, str>),
#[error("Method not allowed: {0}")]
MethodNotAllowed(Cow<'static, str>),
#[error("Request timeout")]
Timeout,
#[error("Payload too large: {0}")]
PayloadTooLarge(Cow<'static, str>),
#[error("Too many requests: {0}")]
TooManyRequests(Cow<'static, str>),
#[error("Internal server error: {0}")]
Internal(Cow<'static, str>),
#[error("Service unavailable: {0}")]
Unavailable(Cow<'static, str>),
#[error("Gateway timeout: {0}")]
GatewayTimeout(Cow<'static, str>),
#[error(transparent)]
#[serde(skip)]
Database(#[from] sqlx::Error),
#[error(transparent)]
#[serde(skip)]
Io(#[from] std::io::Error),
#[error(transparent)]
#[serde(skip)]
Serde(#[from] serde_json::Error),
}
impl AppError {
pub fn not_found(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::NotFound(msg.into())
}
pub fn validation(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::Validation(msg.into())
}
pub fn validation_report(errors: Vec<ValidationError>) -> Self {
AppError::ValidationReport(errors)
}
pub fn unauthorized(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::Unauthorized(msg.into())
}
pub fn internal(msg: impl Into<Cow<'static, str>>) -> Self {
AppError::Internal(msg.into())
}
pub fn status_code(&self) -> u16 {
match self {
AppError::NotFound(_) => 404,
AppError::Validation(_) | AppError::ValidationReport(_) => 400,
AppError::Unauthorized(_) => 401,
AppError::Forbidden => 403,
AppError::Conflict(_) => 409,
AppError::MethodNotAllowed(_) => 405,
AppError::Timeout => 408,
AppError::PayloadTooLarge(_) => 413,
AppError::TooManyRequests(_) => 429,
AppError::Internal(_) => 500,
AppError::Unavailable(_) => 503,
AppError::GatewayTimeout(_) => 504,
AppError::Database(_) | AppError::Io(_) | AppError::Serde(_) => 500,
}
}
pub fn code(&self) -> &'static str {
match self {
AppError::NotFound(_) => "NOT_FOUND",
AppError::Validation(_) | AppError::ValidationReport(_) => "VALIDATION_ERROR",
AppError::Unauthorized(_) => "UNAUTHORIZED",
AppError::Forbidden => "FORBIDDEN",
AppError::Conflict(_) => "CONFLICT",
AppError::MethodNotAllowed(_) => "METHOD_NOT_ALLOWED",
AppError::Timeout => "REQUEST_TIMEOUT",
AppError::PayloadTooLarge(_) => "PAYLOAD_TOO_LARGE",
AppError::TooManyRequests(_) => "TOO_MANY_REQUESTS",
AppError::Internal(_) => "INTERNAL_ERROR",
AppError::Unavailable(_) => "SERVICE_UNAVAILABLE",
AppError::GatewayTimeout(_) => "GATEWAY_TIMEOUT",
AppError::Database(_) => "DATABASE_ERROR",
AppError::Io(_) => "IO_ERROR",
AppError::Serde(_) => "SERIALIZATION_ERROR",
}
}
pub fn field_errors(&self) -> Option<&[ValidationError]> {
match self {
AppError::ValidationReport(errors) => Some(errors),
_ => None,
}
}
pub fn to_response(&self, request_id: Option<&str>) -> HttpResponse {
let details = self.field_errors().map(|errors| {
errors.iter().map(|e| FieldError {
field: e.field.clone(),
message: e.message.clone(),
}).collect()
});
let body = ErrorBody {
error: ErrorDetail {
code: self.code().to_string(),
message: self.to_string(),
request_id: request_id.map(String::from),
details,
},
};
let status = actix_web::http::StatusCode::from_u16(self.status_code())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR);
HttpResponse::build(status).json(&body)
}
}
impl actix_web::ResponseError for AppError {
fn error_response(&self) -> HttpResponse {
self.to_response(None)
}
fn status_code(&self) -> actix_web::http::StatusCode {
actix_web::http::StatusCode::from_u16(self.status_code())
.unwrap_or(actix_web::http::StatusCode::INTERNAL_SERVER_ERROR)
}
}