use axum::{
http::StatusCode,
response::{IntoResponse, Response},
Json,
};
use serde::Serialize;
#[derive(Debug, Clone, Serialize)]
pub struct ProblemDetails {
#[serde(rename = "type")]
pub error_type: String,
pub title: String,
pub status: u16,
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub instance: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub errors: Vec<ValidationIssue>,
}
#[derive(Debug, Clone, Serialize)]
pub struct ValidationIssue {
pub code: String,
pub message: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub location: Option<String>,
}
impl ProblemDetails {
pub fn not_found(detail: impl Into<String>) -> Self {
Self {
error_type: "urn:barbacane:error:not-found".into(),
title: "Not Found".into(),
status: 404,
detail: Some(detail.into()),
instance: None,
errors: vec![],
}
}
pub fn bad_request(detail: impl Into<String>) -> Self {
Self {
error_type: "urn:barbacane:error:bad-request".into(),
title: "Bad Request".into(),
status: 400,
detail: Some(detail.into()),
instance: None,
errors: vec![],
}
}
pub fn conflict(detail: impl Into<String>) -> Self {
Self {
error_type: "urn:barbacane:error:conflict".into(),
title: "Conflict".into(),
status: 409,
detail: Some(detail.into()),
instance: None,
errors: vec![],
}
}
pub fn internal_error() -> Self {
Self {
error_type: "urn:barbacane:error:internal-error".into(),
title: "Internal Server Error".into(),
status: 500,
detail: None,
instance: None,
errors: vec![],
}
}
pub fn internal_error_with_detail(detail: impl Into<String>) -> Self {
Self {
error_type: "urn:barbacane:error:internal-error".into(),
title: "Internal Server Error".into(),
status: 500,
detail: Some(detail.into()),
instance: None,
errors: vec![],
}
}
}
impl IntoResponse for ProblemDetails {
fn into_response(self) -> Response {
let status = StatusCode::from_u16(self.status).unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
let mut response = Json(&self).into_response();
*response.status_mut() = status;
response.headers_mut().insert(
"content-type",
axum::http::HeaderValue::from_static("application/problem+json"),
);
response
}
}
impl From<sqlx::Error> for ProblemDetails {
fn from(err: sqlx::Error) -> Self {
tracing::error!(error = %err, "database error");
match err {
sqlx::Error::RowNotFound => Self::not_found("resource not found"),
sqlx::Error::Database(db_err) => {
if db_err.is_unique_violation() {
Self::conflict("resource already exists")
} else if db_err.is_foreign_key_violation() {
let message = db_err.message();
if message.contains("update or delete")
|| message.contains("is still referenced")
{
Self::conflict("resource is in use and cannot be deleted")
} else {
Self::conflict("referenced resource does not exist")
}
} else {
Self::internal_error()
}
}
_ => Self::internal_error(),
}
}
}