soma_http_api/problem.rs
1//! A minimal, product-neutral structured-error response body.
2//!
3//! Not a full RFC 7807 "problem+json" implementation — just the small
4//! reusable subset every JSON API surface in this workspace already agrees
5//! on: an `error` value and an optional human-readable `message`. Product-
6//! specific error shapes (with retry hints, remediation text, scoped codes,
7//! etc.) stay in the owning product crate and may `From`-convert into this
8//! type at the response boundary.
9//!
10//! `error` is a short machine-readable code (e.g. `"validation_error"`) for
11//! call sites that classify their own failures. For framework-generated
12//! rejections with no natural short code — see `json_rejection_response` in
13//! `response.rs` — it is the framework's raw rejection text instead; there
14//! is no stable code to assign a body-parsing failure that didn't originate
15//! from this crate's own validation logic.
16
17use axum::{
18 http::StatusCode,
19 response::{IntoResponse, Response},
20 Json,
21};
22use serde::Serialize;
23
24/// Generic JSON error body: `{"error": "...", "message": "..."}`.
25#[derive(Debug, Clone, Serialize)]
26pub struct ErrorBody {
27 pub error: String,
28 #[serde(skip_serializing_if = "Option::is_none")]
29 pub message: Option<String>,
30}
31
32impl ErrorBody {
33 pub fn new(error: impl Into<String>) -> Self {
34 Self {
35 error: error.into(),
36 message: None,
37 }
38 }
39
40 #[must_use]
41 pub fn with_message(mut self, message: impl Into<String>) -> Self {
42 self.message = Some(message.into());
43 self
44 }
45
46 /// Render as an Axum response with the given status code.
47 pub fn into_response_with_status(self, status: StatusCode) -> Response {
48 (status, Json(self)).into_response()
49 }
50}
51
52#[cfg(test)]
53mod tests {
54 use super::*;
55
56 #[test]
57 fn error_body_omits_message_when_unset() {
58 let body = ErrorBody::new("bad_request");
59 assert_eq!(
60 serde_json::to_value(&body).unwrap(),
61 serde_json::json!({ "error": "bad_request" })
62 );
63 }
64
65 #[test]
66 fn error_body_includes_message_when_set() {
67 let body = ErrorBody::new("bad_request").with_message("field is required");
68 assert_eq!(
69 serde_json::to_value(&body).unwrap(),
70 serde_json::json!({ "error": "bad_request", "message": "field is required" })
71 );
72 }
73}