use axum::Json;
use axum::http::{HeaderValue, StatusCode, header};
use axum::response::{IntoResponse, Response};
use k2db::{K2DbError, ServiceError as K2ServiceError};
use k2db_api_contract::ProblemDetailsPayload;
#[derive(Debug, Clone)]
pub struct ApiError {
status: StatusCode,
payload: ProblemDetailsPayload,
}
impl ApiError {
pub fn new(
status: StatusCode,
title: impl Into<String>,
detail: impl Into<String>,
trace: impl Into<Option<String>>,
) -> Self {
let title = title.into();
Self {
status,
payload: ProblemDetailsPayload {
type_uri: Some(format!("urn:service-error:{title}")),
title: Some(title),
status: Some(status.as_u16()),
detail: Some(detail.into()),
trace: trace.into(),
chain: None,
extra: serde_json::Map::new(),
},
}
}
pub fn bad_request(detail: impl Into<String>, trace: &'static str) -> Self {
Self::new(StatusCode::BAD_REQUEST, "bad_request", detail, Some(trace.to_owned()))
}
pub fn unauthorized(detail: impl Into<String>, trace: &'static str) -> Self {
Self::new(StatusCode::UNAUTHORIZED, "unauthorized", detail, Some(trace.to_owned()))
}
pub fn forbidden(detail: impl Into<String>, trace: &'static str) -> Self {
Self::new(StatusCode::FORBIDDEN, "forbidden", detail, Some(trace.to_owned()))
}
pub fn internal(detail: impl Into<String>, trace: &'static str) -> Self {
Self::new(
StatusCode::INTERNAL_SERVER_ERROR,
"service_error",
detail,
Some(trace.to_owned()),
)
}
pub fn service_unavailable(detail: impl Into<String>, trace: &'static str) -> Self {
Self::new(
StatusCode::SERVICE_UNAVAILABLE,
"service_unavailable",
detail,
Some(trace.to_owned()),
)
}
pub fn from_k2db(error: K2DbError) -> Self {
let status = match error.service_error {
K2ServiceError::BadRequest => StatusCode::BAD_REQUEST,
K2ServiceError::NotFound => StatusCode::NOT_FOUND,
K2ServiceError::ConfigurationError => StatusCode::INTERNAL_SERVER_ERROR,
K2ServiceError::AlreadyExists => StatusCode::CONFLICT,
K2ServiceError::ValidationError => StatusCode::BAD_REQUEST,
K2ServiceError::BadGateway => StatusCode::BAD_GATEWAY,
K2ServiceError::ServiceUnavailable => StatusCode::SERVICE_UNAVAILABLE,
K2ServiceError::SystemError => StatusCode::INTERNAL_SERVER_ERROR,
};
let title = match error.service_error {
K2ServiceError::BadRequest => "bad_request",
K2ServiceError::NotFound => "not_found",
K2ServiceError::ConfigurationError => "configuration_error",
K2ServiceError::AlreadyExists => "already_exists",
K2ServiceError::ValidationError => "validation_error",
K2ServiceError::BadGateway => "bad_gateway",
K2ServiceError::ServiceUnavailable => "service_unavailable",
K2ServiceError::SystemError => "service_error",
};
Self::new(status, title, error.message, error.key)
}
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
let mut response = (self.status, Json(self.payload)).into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
HeaderValue::from_static("application/problem+json"),
);
response
}
}