2ae9196
ZhenYi
feat(lib/git): add HTTP handler tree with routes and errors across analysis, archives, blobs, branches, commits, conflicts, content, diffs, hooks, lfs, logs, maintenance, packs, patches, pipelines, refs, remotes, repos, signatures, submodules, tags, trees
2ae9196
use std::fmt;
use axum::extract::rejection::{PathRejection, QueryRejection};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use axum::Json;
use export::rest::{AppResponseData, NoData};
use utoipa::IntoResponses;
#[derive(Debug)]
pub enum IssueError {
BadRequest(String),
Unauthorized,
Forbidden,
NotFound,
Conflict(String),
Internal(String),
}
impl fmt::Display for IssueError {
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 IssueError {}
impl From<sea_orm::DbErr> for IssueError {
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<sea_orm::TransactionError<sea_orm::DbErr>> for IssueError {
fn from(error: sea_orm::TransactionError<sea_orm::DbErr>) -> Self {
match &error {
sea_orm::TransactionError::Transaction(db_err) if is_unique_violation(db_err) => {
Self::Conflict("resource already exists".to_string())
}
_ => Self::Internal(format!("database transaction error: {error}")),
}
}
}
impl From<sea_orm::TransactionError<IssueError>> for IssueError {
fn from(error: sea_orm::TransactionError<IssueError>) -> Self {
match error {
sea_orm::TransactionError::Connection(db_err) => Self::from(db_err),
sea_orm::TransactionError::Transaction(issue_err) => issue_err,
}
}
}
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")
}
impl From<space::errors::SpaceError> for IssueError {
fn from(error: space::errors::SpaceError) -> Self {
match error {
space::errors::SpaceError::BadRequest(msg) => Self::BadRequest(msg),
space::errors::SpaceError::Unauthorized => Self::Unauthorized,
space::errors::SpaceError::Forbidden => Self::Forbidden,
space::errors::SpaceError::NotFound => Self::NotFound,
space::errors::SpaceError::Conflict(msg) => Self::Conflict(msg),
space::errors::SpaceError::Internal(msg) => Self::Internal(msg),
}
}
}
impl From<export::ExportError> for IssueError {
fn from(error: export::ExportError) -> Self {
Self::Internal(format!("infrastructure error: {error}"))
}
}
impl From<serde_json::Error> for IssueError {
fn from(error: serde_json::Error) -> Self {
Self::BadRequest(format!("invalid payload: {error}"))
}
}
#[derive(Debug, IntoResponses)]
pub enum ApiError {
#[response(status = 400, description = "request failed validation")]
BadRequest(#[to_schema] AppResponseData<NoData>),
#[response(status = 401, description = "authentication required")]
Unauthorized(#[to_schema] AppResponseData<NoData>),
#[response(status = 403, description = "operation not allowed")]
Forbidden(#[to_schema] AppResponseData<NoData>),
#[response(status = 404, description = "resource not found")]
NotFound(#[to_schema] AppResponseData<NoData>),
#[response(status = 409, description = "resource conflict")]
Conflict(#[to_schema] AppResponseData<NoData>),
#[response(status = 500, description = "internal server error")]
Internal(#[to_schema] AppResponseData<NoData>),
}
impl ApiError {
fn payload(code: i32, message: &'static str) -> AppResponseData<NoData> {
AppResponseData::err(code, message)
}
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 IntoResponse for ApiError {
fn into_response(self) -> Response {
let (code, envelope) = self.decompose();
let status = match StatusCode::from_u16(code) {
Ok(status) => status,
Err(_) => StatusCode::INTERNAL_SERVER_ERROR,
};
(status, Json(envelope)).into_response()
}
}
impl From<IssueError> for ApiError {
fn from(error: IssueError) -> Self {
match error {
IssueError::BadRequest(msg) => Self::BadRequest(AppResponseData::err(400, msg)),
IssueError::Unauthorized => Self::Unauthorized(Self::payload(401, "unauthorized")),
IssueError::Forbidden => Self::Forbidden(Self::payload(403, "forbidden")),
IssueError::NotFound => Self::NotFound(Self::payload(404, "not found")),
IssueError::Conflict(msg) => Self::Conflict(AppResponseData::err(409, msg)),
IssueError::Internal(message) => {
tracing::warn!(error = %message, "issues handler failed");
Self::Internal(Self::payload(500, "internal server error"))
}
}
}
}
impl From<Box<ApiError>> for ApiError {
fn from(error: Box<ApiError>) -> Self {
*error
}
}
impl From<AppResponseData<()>> for ApiError {
fn from(_error: AppResponseData<()>) -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
}
impl From<PathRejection> for ApiError {
fn from(_error: PathRejection) -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
}
impl From<QueryRejection> for ApiError {
fn from(_error: QueryRejection) -> Self {
Self::BadRequest(Self::payload(400, "bad request"))
}
}
impl From<(StatusCode, &'static str)> for ApiError {
fn from(_error: (StatusCode, &'static str)) -> Self {
Self::Internal(Self::payload(500, "internal server error"))
}
}