use axum::Json;
use axum::http::{StatusCode, header};
use axum::response::{IntoResponse, Response};
use serde_json::json;
pub const PROBLEM_TYPE_BASE: &str = "https://factory0.ventures/problems/";
#[derive(Debug, Clone)]
pub struct Problem {
pub slug: &'static str,
pub status: StatusCode,
pub title: &'static str,
pub detail: Option<String>,
pub instance: Option<String>,
}
impl Problem {
pub fn new(def: &crate::problems::ProblemDef) -> Self {
Self {
slug: def.slug,
status: def.status,
title: def.title,
detail: None,
instance: None,
}
}
#[must_use]
pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
self.detail = Some(detail.into());
self
}
#[must_use]
pub fn instance(mut self, request_id: &str) -> Self {
self.instance = Some(request_id.to_string());
self
}
pub fn internal() -> Self {
Self::new(&crate::problems::SLUGS.internal)
}
pub fn validation_failed(detail: impl Into<String>) -> Self {
Self::new(&crate::problems::SLUGS.validation_failed).with_detail(detail)
}
pub fn request_too_large() -> Self {
Self::new(&crate::problems::SLUGS.request_too_large)
}
pub fn not_ready(detail: impl Into<String>) -> Self {
Self::new(&crate::problems::SLUGS.not_ready).with_detail(detail)
}
pub fn not_found() -> Self {
Self::new(&crate::problems::SLUGS.not_found)
}
pub fn type_uri(&self) -> String {
format!("{PROBLEM_TYPE_BASE}{}", self.slug)
}
}
impl IntoResponse for Problem {
fn into_response(self) -> Response {
let mut body = json!({
"type": self.type_uri(),
"title": self.title,
"status": self.status.as_u16(),
});
if let Some(detail) = &self.detail {
body["detail"] = json!(detail);
}
if let Some(instance) = &self.instance {
body["instance"] = json!(instance);
}
let mut response = (self.status, Json(body)).into_response();
response.headers_mut().insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static("application/problem+json"),
);
response
}
}
impl std::fmt::Display for Problem {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} ({})", self.type_uri(), self.status.as_u16())
}
}
impl std::error::Error for Problem {}
impl From<crate::ports::DbError> for Problem {
fn from(error: crate::ports::DbError) -> Self {
tracing::error!(error = %error, "database error mapped to internal problem");
Self::internal()
}
}