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 UserError {
BadRequest(String),
Unauthorized,
Forbidden,
NotFound,
Conflict(String),
Internal(String),
}
impl fmt::Display for UserError {
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 UserError {}
impl From<sea_orm::DbErr> for UserError {
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 UserError {
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<UserError>> for UserError {
fn from(error: sea_orm::TransactionError<UserError>) -> Self {
match error {
sea_orm::TransactionError::Connection(db_err) => Self::from(db_err),
sea_orm::TransactionError::Transaction(user_err) => user_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<storage::StorageError> for UserError {
fn from(error: storage::StorageError) -> Self {
Self::Internal(format!("storage error: {error}"))
}
}
impl From<redis::RedisError> for UserError {
fn from(error: redis::RedisError) -> Self {
Self::Internal(format!("redis error: {error}"))
}
}
impl From<export::ExportError> for UserError {
fn from(error: export::ExportError) -> Self {
Self::Internal(format!("infrastructure error: {error}"))
}
}
impl From<serde_json::Error> for UserError {
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<UserError> for ApiError {
fn from(error: UserError) -> Self {
match error {
UserError::BadRequest(msg) => Self::BadRequest(AppResponseData::err(400, msg)),
UserError::Unauthorized => Self::Unauthorized(Self::payload(401, "unauthorized")),
UserError::Forbidden => Self::Forbidden(Self::payload(403, "forbidden")),
UserError::NotFound => Self::NotFound(Self::payload(404, "not found")),
UserError::Conflict(msg) => Self::Conflict(AppResponseData::err(409, msg)),
UserError::Internal(message) => {
tracing::warn!(error = %message, "user 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"))
}
}