use axum::Json; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use serde::Serialize; use thiserror::Error; pub type RepoResult = Result; #[derive(Debug, Error)] pub enum RepoError { #[error("bad request: {0}")] BadRequest(String), #[error("unauthorized: {0}")] Unauthorized(String), #[error("forbidden: {0}")] Forbidden(String), #[error("not found: {0}")] NotFound(String), #[error("conflict: {0}")] Conflict(String), #[error("unprocessable: {0}")] Unprocessable(String), #[error("rate limited: {0}")] RateLimited(String), #[error("internal: {0}")] Internal(String), #[error("database error: {0}")] Sqlx(#[from] db::conn::SqlxError), #[error("uuid: {0}")] Uuid(#[from] uuid::Error), #[error("git error: {0}")] Git(#[from] git::Error), } impl From for RepoError { fn from(re: axum::extract::rejection::JsonRejection) -> Self { RepoError::BadRequest(re.body_text()) } } impl From for RepoError { fn from(re: axum::extract::rejection::PathRejection) -> Self { RepoError::BadRequest(re.body_text()) } } impl From for RepoError { fn from(re: axum::extract::rejection::QueryRejection) -> Self { RepoError::BadRequest(re.body_text()) } } impl RepoError { fn status(&self) -> StatusCode { match self { RepoError::BadRequest(_) => StatusCode::BAD_REQUEST, RepoError::Unauthorized(_) => StatusCode::UNAUTHORIZED, RepoError::Forbidden(_) => StatusCode::FORBIDDEN, RepoError::NotFound(_) => StatusCode::NOT_FOUND, RepoError::Conflict(_) => StatusCode::CONFLICT, RepoError::Unprocessable(_) => StatusCode::UNPROCESSABLE_ENTITY, RepoError::RateLimited(_) => StatusCode::TOO_MANY_REQUESTS, RepoError::Internal(_) | RepoError::Sqlx(_) | RepoError::Uuid(_) | RepoError::Git(_) => StatusCode::INTERNAL_SERVER_ERROR, } } fn is_user_facing(&self) -> bool { matches!( self, RepoError::BadRequest(_) | RepoError::Unauthorized(_) | RepoError::Forbidden(_) | RepoError::NotFound(_) | RepoError::Conflict(_) | RepoError::Unprocessable(_) | RepoError::RateLimited(_) ) } } #[derive(Serialize)] struct ErrorBody<'a> { error: &'a str, } impl IntoResponse for RepoError { fn into_response(self) -> Response { let status = self.status(); let msg = if self.is_user_facing() { self.to_string() } else { tracing::error!(error = %self, "repo internal error"); "internal error".to_string() }; let body = ErrorBody { error: &msg }; (status, Json(body)).into_response() } }