Skip to main content

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}