use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response as AxumResponse},
};
use serde_json::json;
use std::fmt;
#[derive(thiserror::Error, Debug)]
pub enum RouteError {
#[error("Bad request: {0}")]
BadRequest(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Unauthorized: {0}")]
Unauthorized(String),
#[error("Forbidden: {0}")]
Forbidden(String),
#[error("Method not allowed: {0}")]
MethodNotAllowed(String),
#[error("Conflict: {0}")]
Conflict(String),
#[error("Validation error: {0}")]
Validation(String),
#[error("Too many requests: {0}")]
RateLimit(String),
#[error("Internal error: {0}")]
Internal(#[from] anyhow::Error),
#[error("Error {status}: {message}")]
Custom { status: StatusCode, message: String },
}
impl RouteError {
pub fn bad_request<M: fmt::Display>(message: M) -> Self {
Self::BadRequest(message.to_string())
}
pub fn not_found<M: fmt::Display>(message: M) -> Self {
Self::NotFound(message.to_string())
}
pub fn unauthorized<M: fmt::Display>(message: M) -> Self {
Self::Unauthorized(message.to_string())
}
pub fn forbidden<M: fmt::Display>(message: M) -> Self {
Self::Forbidden(message.to_string())
}
pub fn conflict<M: fmt::Display>(message: M) -> Self {
Self::Conflict(message.to_string())
}
pub fn validation<M: fmt::Display>(message: M) -> Self {
Self::Validation(message.to_string())
}
pub fn rate_limit<M: fmt::Display>(message: M) -> Self {
Self::RateLimit(message.to_string())
}
pub fn custom<M: fmt::Display>(status: StatusCode, message: M) -> Self {
Self::Custom {
status,
message: message.to_string(),
}
}
#[must_use]
pub fn status_code(&self) -> StatusCode {
match self {
Self::BadRequest(_) => StatusCode::BAD_REQUEST,
Self::NotFound(_) => StatusCode::NOT_FOUND,
Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
Self::Forbidden(_) => StatusCode::FORBIDDEN,
Self::MethodNotAllowed(_) => StatusCode::METHOD_NOT_ALLOWED,
Self::Conflict(_) => StatusCode::CONFLICT,
Self::Validation(_) => StatusCode::UNPROCESSABLE_ENTITY,
Self::RateLimit(_) => StatusCode::TOO_MANY_REQUESTS,
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
Self::Custom { status, .. } => *status,
}
}
#[must_use]
pub fn message(&self) -> String {
match self {
Self::BadRequest(msg)
| Self::NotFound(msg)
| Self::Unauthorized(msg)
| Self::Forbidden(msg)
| Self::MethodNotAllowed(msg)
| Self::Conflict(msg)
| Self::Validation(msg)
| Self::RateLimit(msg)
| Self::Custom { message: msg, .. } => msg.clone(),
Self::Internal(e) => e.to_string(),
}
}
#[must_use]
pub fn is_client_error(&self) -> bool {
self.status_code().is_client_error()
}
#[must_use]
pub fn is_server_error(&self) -> bool {
self.status_code().is_server_error()
}
}
impl IntoResponse for RouteError {
fn into_response(self) -> AxumResponse {
let status = self.status_code();
let body = json!({
"error": self.message(),
"status": status.as_u16(),
});
(status, Json(body)).into_response()
}
}
pub type Result<T> = std::result::Result<T, RouteError>;