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 = Result; #[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 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 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>), #[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 envelope(code: i32, message: impl Into) -> Box> { Box::new(AppResponseData::err(code, message)) } pub fn bad_request(message: impl Into) -> 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) -> Self { Self::Conflict(Self::envelope(409, message)) } pub fn internal() -> Self { Self::Internal(Self::envelope(500, "internal server error")) } fn decompose(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 From for ApiError { fn from(error: CiError) -> Self { Self::from(CiApiError::from(error)) } } impl From 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() } }