2687ace
use std::fmt;
use axum::Json;
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use export::rest::{AppResponseData, NoData};
use utoipa::IntoResponses;
#[derive(Debug, thiserror::Error)]
pub enum CiError {
#[error("invalid argument: {0}")]
InvalidArgument(String),
#[error("conflict: {0}")]
Conflict(String),
#[error("resource not found")]
NotFound,
#[error("unauthorized")]
Unauthorized,
#[error("forbidden")]
Forbidden,
#[error("git error: {0}")]
Git(#[from] git::share::errors::GitError),
#[error("storage error: {0}")]
Storage(#[from] storage::StorageError),
#[error("database error: {0}")]
Db(#[from] sea_orm::DbErr),
#[error("serialization error: {0}")]
Serde(#[from] serde_json::Error),
#[error("internal error: {0}")]
Internal(String),
}
pub type CiResult<T> = Result<T, CiError>;
#[derive(Debug)]
pub enum CiApiError {
BadRequest(String),
Unauthorized,
Forbidden,
NotFound,
Conflict(String),
Internal(String),
}
impl fmt::Display for CiApiError {
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 CiApiError {}
impl From<CiError> for CiApiError {
fn from(error: CiError) -> Self {
match error {
CiError::NotFound => Self::NotFound,
CiError::Unauthorized => Self::Unauthorized,
CiError::Forbidden => Self::Forbidden,
CiError::Conflict(msg) => Self::Conflict(msg),
CiError::InvalidArgument(msg) => Self::BadRequest(msg),
other => {
tracing::warn!(error = %other, "ci handler failed");
Self::Internal("internal server error".to_string())
}
}
}
}
impl From<sea_orm::DbErr> for CiApiError {
fn from(error: sea_orm::DbErr) -> Self {
Self::Internal(format!("database error: {error}"))
}
}
#[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 envelope(code: i32, message: impl Into<String>) -> Box<AppResponseData<NoData>> {
Box::new(AppResponseData::err(code, message))
}
pub fn bad_request(message: impl Into<String>) -> Self {
Self::BadRequest(Self::envelope(400, message))
}
pub fn unauthorized() -> Self {
Self::Unauthorized(Self::envelope(401, "unauthorized"))
}
pub fn forbidden() -> Self {
Self::Forbidden(Self::envelope(403, "forbidden"))
}
pub fn not_found() -> Self {
Self::NotFound(Self::envelope(404, "not found"))
}
pub fn conflict(message: impl Into<String>) -> Self {
Self::Conflict(Self::envelope(409, message))
}
pub fn internal() -> Self {
Self::Internal(Self::envelope(500, "internal server error"))
}
fn decompose(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 From<CiError> for ApiError {
fn from(error: CiError) -> Self {
Self::from(CiApiError::from(error))
}
}
impl From<CiApiError> for ApiError {
fn from(error: CiApiError) -> Self {
match error {
CiApiError::BadRequest(message) => Self::bad_request(message),
CiApiError::Unauthorized => Self::unauthorized(),
CiApiError::Forbidden => Self::forbidden(),
CiApiError::NotFound => Self::not_found(),
CiApiError::Conflict(message) => Self::conflict(message),
CiApiError::Internal(message) => {
tracing::warn!(message, "ci internal error");
Self::internal()
}
}
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let (code, envelope) = self.decompose();
let status = StatusCode::from_u16(code).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(status, Json(envelope)).into_response()
}
}