cratefield_core/
problem.rs1use axum::Json;
4use axum::http::{StatusCode, header};
5use axum::response::{IntoResponse, Response};
6use serde_json::json;
7
8pub const PROBLEM_TYPE_BASE: &str = "https://factory0.ventures/problems/";
11
12#[derive(Debug, Clone)]
18pub struct Problem {
19 pub slug: &'static str,
20 pub status: StatusCode,
21 pub title: &'static str,
22 pub detail: Option<String>,
23 pub instance: Option<String>,
24}
25
26impl Problem {
27 pub fn new(def: &crate::problems::ProblemDef) -> Self {
28 Self {
29 slug: def.slug,
30 status: def.status,
31 title: def.title,
32 detail: None,
33 instance: None,
34 }
35 }
36
37 #[must_use]
38 pub fn with_detail(mut self, detail: impl Into<String>) -> Self {
39 self.detail = Some(detail.into());
40 self
41 }
42
43 #[must_use]
45 pub fn instance(mut self, request_id: &str) -> Self {
46 self.instance = Some(request_id.to_string());
47 self
48 }
49
50 pub fn internal() -> Self {
51 Self::new(&crate::problems::SLUGS.internal)
52 }
53
54 pub fn validation_failed(detail: impl Into<String>) -> Self {
55 Self::new(&crate::problems::SLUGS.validation_failed).with_detail(detail)
56 }
57
58 pub fn request_too_large() -> Self {
59 Self::new(&crate::problems::SLUGS.request_too_large)
60 }
61
62 pub fn not_ready(detail: impl Into<String>) -> Self {
63 Self::new(&crate::problems::SLUGS.not_ready).with_detail(detail)
64 }
65
66 pub fn not_found() -> Self {
67 Self::new(&crate::problems::SLUGS.not_found)
68 }
69
70 pub fn type_uri(&self) -> String {
71 format!("{PROBLEM_TYPE_BASE}{}", self.slug)
72 }
73}
74
75impl IntoResponse for Problem {
76 fn into_response(self) -> Response {
77 let mut body = json!({
78 "type": self.type_uri(),
79 "title": self.title,
80 "status": self.status.as_u16(),
81 });
82 if let Some(detail) = &self.detail {
83 body["detail"] = json!(detail);
84 }
85 if let Some(instance) = &self.instance {
86 body["instance"] = json!(instance);
87 }
88 let mut response = (self.status, Json(body)).into_response();
89 response.headers_mut().insert(
90 header::CONTENT_TYPE,
91 header::HeaderValue::from_static("application/problem+json"),
92 );
93 response
94 }
95}
96
97impl std::fmt::Display for Problem {
98 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
99 write!(f, "{} ({})", self.type_uri(), self.status.as_u16())
100 }
101}
102
103impl std::error::Error for Problem {}
104
105impl From<crate::ports::DbError> for Problem {
109 fn from(error: crate::ports::DbError) -> Self {
110 tracing::error!(error = %error, "database error mapped to internal problem");
111 Self::internal()
112 }
113}