use std::fmt; use actix_web::http::StatusCode; use api::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 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 for GitError { fn from(error: dir::Error) -> Self { match error { dir::Error::InvalidInput(message) => Self::InvalidArgument(message), dir::Error::NotFound => Self::NotFound, dir::Error::Database(error) => Self::from(error), } } } impl From 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 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>), #[response(status = 401, description = "authentication required")] Unauthorized(#[to_schema] Box>), #[response(status = 403, description = "operation not allowed")] Forbidden(#[to_schema] Box>), #[response(status = 404, description = "resource not found")] NotFound(#[to_schema] Box>), #[response(status = 409, description = "resource conflict")] Conflict(#[to_schema] Box>), #[response(status = 500, description = "internal server error")] Internal(#[to_schema] Box>), } impl ApiError { /// Boxes a plain envelope into the enum's payload. The box keeps /// `Result` 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> { 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) -> 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) { 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 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")) } } } }