Skip to content
Clone
07f791d
170 lines 5.9 KB Rust
use std::fmt;
use actix_web::http::StatusCode;
use export::rest::{AppResponseData, NoData};
use utoipa::IntoResponses;
use crate::share::errors::GitError;
#[derive(Debug)]
pub enum GitApiError {
BadRequest(String),
Unauthorized,
Forbidden,
NotFound,
Conflict(String),
Internal(String),
}
impl fmt::Display for GitApiError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::BadRequest(message) => write!(f, "{message}"),
Self::Unauthorized => write!(f, "unauthorized"),
Self::Forbidden => write!(f, "forbidden"),
Self::NotFound => write!(f, "not found"),
Self::Conflict(message) => write!(f, "{message}"),
Self::Internal(message) => write!(f, "{message}"),
}
}
}
impl std::error::Error for GitApiError {}
impl From<GitError> for GitApiError {
fn from(error: GitError) -> Self {
match error {
GitError::NotFound => Self::NotFound,
GitError::Unauthorized => Self::Unauthorized,
GitError::Forbidden => Self::Forbidden,
GitError::Conflict(msg) => Self::Conflict(msg),
GitError::InvalidArgument(msg) => Self::BadRequest(msg),
GitError::Unimplemented(msg) => Self::Internal(msg.to_string()),
GitError::Cache(_) | GitError::Git(_) | GitError::Internal(_) => {
tracing::warn!(error = %error, "git handler failed");
Self::Internal("internal server error".to_string())
}
}
}
}
impl From<sea_orm::DbErr> for GitApiError {
fn from(error: sea_orm::DbErr) -> Self {
if is_unique_violation(&error) {
return Self::Conflict("resource already exists".to_string());
}
Self::Internal(format!("database error: {error}"))
}
}
impl From<serde_json::Error> for GitApiError {
fn from(error: serde_json::Error) -> Self {
Self::BadRequest(format!("invalid payload: {error}"))
}
}
fn is_unique_violation(err: &sea_orm::DbErr) -> bool {
let msg = err.to_string().to_lowercase();
msg.contains("unique") || msg.contains("duplicate") || msg.contains("23505")
}
#[derive(Debug, IntoResponses)]
pub enum ApiError {
#[response(status = 400, description = "request failed validation")]
BadRequest(#[to_schema] Box<AppResponseData<NoData>>),
#[response(status = 401, description = "authentication required")]
Unauthorized(#[to_schema] Box<AppResponseData<NoData>>),
#[response(status = 403, description = "operation not allowed")]
Forbidden(#[to_schema] Box<AppResponseData<NoData>>),
#[response(status = 404, description = "resource not found")]
NotFound(#[to_schema] Box<AppResponseData<NoData>>),
#[response(status = 409, description = "resource conflict")]
Conflict(#[to_schema] Box<AppResponseData<NoData>>),
#[response(status = 500, description = "internal server error")]
Internal(#[to_schema] Box<AppResponseData<NoData>>),
}
impl ApiError {
/// Boxes a plain envelope into the enum's payload. The box keeps
/// `Result<T, ApiError>` small: an inline envelope is 128 bytes, which every
/// handler return type would otherwise carry on the happy path too.
fn payload(code: i32, message: &'static str) -> Box<AppResponseData<NoData>> {
Box::new(AppResponseData::err(code, message))
}
}
impl ApiError {
pub fn bad_request() -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
pub fn unauthorized() -> Self {
Self::Unauthorized(Self::payload(401, "unauthorized"))
}
pub fn forbidden() -> Self {
Self::Forbidden(Self::payload(403, "forbidden"))
}
pub fn not_found() -> Self {
Self::NotFound(Self::payload(404, "not found"))
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::Conflict(Box::new(AppResponseData::err(409, message)))
}
pub fn internal() -> Self {
Self::Internal(Self::payload(500, "internal server error"))
}
}
impl std::fmt::Display for ApiError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let (_, envelope) = self.parts();
formatter.write_str(&envelope.message)
}
}
impl ApiError {
fn parts(&self) -> (u16, &AppResponseData<NoData>) {
match self {
Self::BadRequest(envelope) => (400, envelope),
Self::Unauthorized(envelope) => (401, envelope),
Self::Forbidden(envelope) => (403, envelope),
Self::NotFound(envelope) => (404, envelope),
Self::Conflict(envelope) => (409, envelope),
Self::Internal(envelope) => (500, envelope),
}
}
}
impl actix_web::ResponseError for ApiError {
fn status_code(&self) -> StatusCode {
StatusCode::from_u16(self.parts().0).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR)
}
fn error_response(&self) -> actix_web::HttpResponse {
let (status, envelope) = self.parts();
let status = StatusCode::from_u16(status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
actix_web::HttpResponse::build(status).json(envelope)
}
}
impl From<GitApiError> for ApiError {
fn from(error: GitApiError) -> Self {
match error {
GitApiError::BadRequest(msg) => {
Self::BadRequest(Box::new(AppResponseData::err(400, msg)))
}
GitApiError::Unauthorized => Self::Unauthorized(Self::payload(401, "unauthorized")),
GitApiError::Forbidden => Self::Forbidden(Self::payload(403, "forbidden")),
GitApiError::NotFound => Self::NotFound(Self::payload(404, "not found")),
GitApiError::Conflict(msg) => Self::Conflict(Box::new(AppResponseData::err(409, msg))),
GitApiError::Internal(message) => {
tracing::warn!(error = %message, "git handler failed");
Self::Internal(Self::payload(500, "internal server error"))
}
}
}
}