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;
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::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] 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 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(AppResponseData::err(409, message))
}
pub fn internal() -> Self {
Self::Internal(Self::payload(500, "internal server error"))
}
}
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<GitApiError> for ApiError {
fn from(error: GitApiError) -> Self {
match error {
GitApiError::BadRequest(msg) => Self::BadRequest(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(AppResponseData::err(409, msg)),
GitApiError::Internal(message) => {
tracing::warn!(error = %message, "git handler failed");
Self::Internal(Self::payload(500, "internal server error"))
}
}
}
}
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"))
}
}