use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::{Deserialize, Serialize};
use std::fmt;
#[derive(Debug)]
pub enum ApiError {
BadRequest(String),
Unauthorized(String),
Forbidden(String),
NotFound(String),
Conflict(String),
UnprocessableEntity(String),
TooManyRequests(String),
InternalServerError(String),
ServiceUnavailable(String),
ValidationError(Vec<ValidationError>),
DatabaseError(String),
DomainError(String),
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadRequest(msg) => write!(f, "Bad request: {}", msg),
Self::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
Self::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
Self::NotFound(msg) => write!(f, "Not found: {}", msg),
Self::Conflict(msg) => write!(f, "Conflict: {}", msg),
Self::UnprocessableEntity(msg) => write!(f, "Unprocessable entity: {}", msg),
Self::TooManyRequests(msg) => write!(f, "Too many requests: {}", msg),
Self::InternalServerError(msg) => write!(f, "Internal server error: {}", msg),
Self::ServiceUnavailable(msg) => write!(f, "Service unavailable: {}", msg),
Self::ValidationError(errors) => write!(f, "Validation error: {:?}", errors),
Self::DatabaseError(msg) => write!(f, "Database error: {}", msg),
Self::DomainError(msg) => write!(f, "Domain error: {}", msg),
}
}
}
impl std::error::Error for ApiError {}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ValidationError {
pub field: String,
pub message: String,
pub code: Option<String>,
}
impl ValidationError {
pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
field: field.into(),
message: message.into(),
code: None,
}
}
pub fn with_code(mut self, code: impl Into<String>) -> Self {
self.code = Some(code.into());
self
}
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorResponse {
pub error: ErrorDetail,
#[serde(skip_serializing_if = "Option::is_none")]
pub request_id: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct ErrorDetail {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub details: Option<serde_json::Value>,
}
impl ApiError {
pub fn status_code(&self) -> StatusCode {
match self {
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
Self::Forbidden(_) => StatusCode::FORBIDDEN,
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::Conflict(_) => StatusCode::CONFLICT,
Self::UnprocessableEntity(_) | Self::ValidationError(_) => {
StatusCode::UNPROCESSABLE_ENTITY
}
Self::TooManyRequests(_) => StatusCode::TOO_MANY_REQUESTS,
Self::InternalServerError(_) | Self::DatabaseError(_) | Self::DomainError(_) => {
StatusCode::INTERNAL_SERVER_ERROR
}
Self::ServiceUnavailable(_) => StatusCode::SERVICE_UNAVAILABLE,
}
}
pub fn error_code(&self) -> String {
match self {
Self::BadRequest(_) => "BAD_REQUEST",
Self::Unauthorized(_) => "UNAUTHORIZED",
Self::Forbidden(_) => "FORBIDDEN",
Self::NotFound(_) => "NOT_FOUND",
Self::Conflict(_) => "CONFLICT",
Self::UnprocessableEntity(_) => "UNPROCESSABLE_ENTITY",
Self::ValidationError(_) => "VALIDATION_ERROR",
Self::TooManyRequests(_) => "TOO_MANY_REQUESTS",
Self::InternalServerError(_) => "INTERNAL_SERVER_ERROR",
Self::ServiceUnavailable(_) => "SERVICE_UNAVAILABLE",
Self::DatabaseError(_) => "DATABASE_ERROR",
Self::DomainError(_) => "DOMAIN_ERROR",
}
.to_string()
}
pub fn to_response(&self) -> ErrorResponse {
let details = match self {
Self::ValidationError(errors) => Some(serde_json::to_value(errors).unwrap()),
_ => None,
};
ErrorResponse {
error: ErrorDetail {
code: self.error_code(),
message: self.to_string(),
details,
},
request_id: None,
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let status = self.status_code();
let body = Json(self.to_response());
(status, body).into_response()
}
}
impl From<llm_cost_ops::CostOpsError> for ApiError {
fn from(err: llm_cost_ops::CostOpsError) -> Self {
Self::DomainError(err.to_string())
}
}
impl From<sqlx::Error> for ApiError {
fn from(err: sqlx::Error) -> Self {
match err {
sqlx::Error::RowNotFound => Self::NotFound("Resource not found".to_string()),
_ => Self::DatabaseError(err.to_string()),
}
}
}
impl From<serde_json::Error> for ApiError {
fn from(err: serde_json::Error) -> Self {
Self::BadRequest(format!("Invalid JSON: {}", err))
}
}
impl From<validator::ValidationErrors> for ApiError {
fn from(errors: validator::ValidationErrors) -> Self {
let validation_errors: Vec<ValidationError> = errors
.field_errors()
.iter()
.flat_map(|(field, errors)| {
errors.iter().map(move |error| {
ValidationError::new(
field.to_string(),
error.message.clone().unwrap_or_default().to_string(),
)
.with_code(error.code.to_string())
})
})
.collect();
Self::ValidationError(validation_errors)
}
}
pub type ApiResult<T> = Result<T, ApiError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_api_error_status_codes() {
assert_eq!(
ApiError::BadRequest("test".to_string()).status_code(),
StatusCode::BAD_REQUEST
);
assert_eq!(
ApiError::Unauthorized("test".to_string()).status_code(),
StatusCode::UNAUTHORIZED
);
assert_eq!(
ApiError::NotFound("test".to_string()).status_code(),
StatusCode::NOT_FOUND
);
assert_eq!(
ApiError::InternalServerError("test".to_string()).status_code(),
StatusCode::INTERNAL_SERVER_ERROR
);
}
#[test]
fn test_validation_error_creation() {
let error = ValidationError::new("email", "Invalid email format")
.with_code("INVALID_EMAIL");
assert_eq!(error.field, "email");
assert_eq!(error.message, "Invalid email format");
assert_eq!(error.code, Some("INVALID_EMAIL".to_string()));
}
#[test]
fn test_error_response_serialization() {
let api_error = ApiError::BadRequest("Invalid request".to_string());
let response = api_error.to_response();
assert_eq!(response.error.code, "BAD_REQUEST");
assert!(response.error.message.contains("Invalid request"));
}
}