Skip to main content

shared_framework/response/
mod.rs

1//! Typed HTTP responses: success payloads, errors, and content types.
2//!
3//! Handlers return `Result<ServiceResult<T>, ErrorResult>`: [`ServiceResult`] is the
4//! standard JSON envelope (`status`/`message`/`data`) plus status code and content type,
5//! and [`ErrorResult`] is its fallible counterpart. [`TypedServiceResult`] is the
6//! object-safe trait both implement, so handlers needing heterogeneous payloads can
7//! return `Box<dyn TypedServiceResult>` (see [`ServiceResult::boxed`]). Use
8//! [`build_response`] / [`error_response`] to render them into `http::Response<String>`
9//! with correlation headers.
10//! ```ignore
11//! async fn get_user(ctx: CorrelationContext) -> Result<ServiceResult<User>, ErrorResult> {
12//!     let user = ctx.body::<User>()?;
13//!     Ok(ServiceResult::ok("Fetched", user))
14//! }
15//! ```
16
17pub mod html;
18
19use serde::{Deserialize, Serialize};
20use serde_json::Value;
21use std::sync::Arc;
22
23/// Content type used when rendering a result: selects the `Content-Type` header,
24/// and `File` additionally sets `Content-Disposition: attachment`.
25#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
26pub enum ResponseType {
27    /// `application/json` (default).
28    Json,
29    /// `text/plain`.
30    Text,
31    /// `text/html`.
32    Html,
33    /// `application/xml`.
34    Xml,
35    /// `application/javascript`.
36    Javascript,
37    /// `application/octet-stream` with an attachment filename.
38    File,
39}
40
41impl Default for ResponseType {
42    fn default() -> Self {
43        Self::Json
44    }
45}
46
47/// Renderable handler result: message, HTTP status, content type, and body serializer.
48/// Implemented by [`ServiceResult`], [`GenericTypedResult`], and [`ErrorResult`], so
49/// handlers can return them as `Box<dyn TypedServiceResult>` for mixed payload shapes.
50pub trait TypedServiceResult: Send + Sync {
51    /// Human-readable message carried by the result.
52    fn message(&self) -> &str;
53    /// HTTP status code used for the response.
54    fn code(&self) -> u16;
55    /// Content type used for the `Content-Type` header.
56    fn response_type(&self) -> ResponseType;
57    /// Renders the response body; I/O or shape errors surface as `serde_json::Error`.
58    fn serialize(&self) -> Result<String, serde_json::Error>;
59    /// Returns the payload as JSON when representable; defaults to `None`.
60    fn data_json(&self) -> Option<Value> {
61        None
62    }
63}
64
65/// Standard success envelope: serializes as `{"status","message","data"}` unless
66/// [`ServiceResult::stripped`] is used, in which case only `data` is serialized.
67/// Type parameter `T` is the `data` payload. `code` and `response_type` control the
68/// HTTP status and content type but are not serialized into the body.
69#[derive(Debug, Clone, Serialize)]
70pub struct ServiceResult<T: Serialize> {
71    /// Envelope status label (e.g. `"success"`).
72    pub status: String,
73    /// Human-readable message.
74    pub message: String,
75    /// Optional payload serialized as `data`.
76    pub data: Option<T>,
77    #[serde(skip)]
78    /// HTTP status code for the response.
79    pub code: u16,
80    #[serde(skip)]
81    /// Content type used when rendering the response.
82    pub response_type: ResponseType,
83    #[serde(skip)]
84    naked: bool,
85}
86
87impl<T: Serialize> ServiceResult<T> {
88    /// Creates a result with an explicit status label, message, optional payload, and HTTP code.
89    pub fn new(
90        status: impl Into<String>,
91        message: impl Into<String>,
92        data: Option<T>,
93        code: u16,
94    ) -> Self {
95        Self {
96            status: status.into(),
97            message: message.into(),
98            data,
99            code,
100            response_type: ResponseType::Json,
101            naked: false,
102        }
103    }
104    /// Creates a 200 success result with status `"success"`, the given message, and `data`.
105    pub fn ok(message: impl Into<String>, data: T) -> Self {
106        Self::new("success", message, Some(data), 200)
107    }
108    /// Creates a 200 success result with status `"success"` and no payload.
109    pub fn ok_empty(message: impl Into<String>) -> Self
110    where
111        T: Default,
112    {
113        Self::new("success", message, None, 200)
114    }
115    /// Serializes only the `data` field instead of the `status`/`message`/`data` envelope.
116    pub fn stripped(mut self) -> Self {
117        self.naked = true;
118        self
119    }
120    /// Overrides the content type used when rendering this result.
121    pub fn with_response_type(mut self, rt: ResponseType) -> Self {
122        self.response_type = rt;
123        self
124    }
125    /// Overrides the HTTP status code used when rendering this result.
126    pub fn with_code(mut self, code: u16) -> Self {
127        self.code = code;
128        self
129    }
130
131    /// Boxes this result so one handler can return different `ServiceResult<T>` payload shapes.
132    pub fn boxed(self) -> Box<dyn TypedServiceResult>
133    where
134        T: Send + Sync + 'static,
135    {
136        Box::new(self)
137    }
138
139    /// Serializes the result body directly; the trait `serialize` delegates to this.
140    /// A stripped result serializes only `data`, otherwise the full envelope is used.
141    pub fn serialize_inherent(&self) -> Result<String, serde_json::Error> {
142        if self.naked {
143            return serde_json::to_string(&self.data);
144        }
145        let wrapper = serde_json::json!({ "status": self.status, "message": self.message, "data": self.data });
146        serde_json::to_string(&wrapper)
147    }
148}
149
150impl<T: Serialize + Send + Sync> TypedServiceResult for ServiceResult<T> {
151    fn message(&self) -> &str {
152        &self.message
153    }
154    fn code(&self) -> u16 {
155        self.code
156    }
157    fn response_type(&self) -> ResponseType {
158        self.response_type
159    }
160    fn serialize(&self) -> Result<String, serde_json::Error> {
161        self.serialize_inherent()
162    }
163    fn data_json(&self) -> Option<Value> {
164        self.data
165            .as_ref()
166            .and_then(|d| serde_json::to_value(d).ok())
167    }
168}
169
170/// Generic JSON-compatible result: serializes its `data` value directly, or `"null"` when absent.
171/// Useful for endpoints whose payload is already a `serde_json::Value`.
172pub struct GenericTypedResult {
173    /// Human-readable message (used if serialization fails upstream).
174    pub message: String,
175    /// Payload serialized directly as the body.
176    pub data: Option<Value>,
177    /// HTTP status code for the response.
178    pub code: u16,
179    /// Content type used when rendering the response.
180    pub response_type: ResponseType,
181}
182
183impl TypedServiceResult for GenericTypedResult {
184    fn message(&self) -> &str {
185        &self.message
186    }
187    fn code(&self) -> u16 {
188        self.code
189    }
190    fn response_type(&self) -> ResponseType {
191        self.response_type
192    }
193    fn serialize(&self) -> Result<String, serde_json::Error> {
194        match &self.data {
195            Some(v) => serde_json::to_string(v),
196            None => Ok("null".to_string()),
197        }
198    }
199}
200
201/// Handler failure value: message, optional JSON payload, and HTTP status code.
202/// Returned as `Err` from handlers and rendered as an `{"status":"error",...}` envelope.
203#[derive(Debug, Clone, Serialize)]
204pub struct ErrorResult {
205    /// Human-readable error message.
206    pub message: String,
207    /// Optional extra error payload.
208    pub data: Option<Value>,
209    /// HTTP status code for the response.
210    pub code: u16,
211}
212
213impl ErrorResult {
214    /// Creates an error with an explicit message, optional data, and HTTP status `code`.
215    pub fn new(message: impl Into<String>, data: Option<Value>, code: u16) -> Self {
216        Self {
217            message: message.into(),
218            data,
219            code,
220        }
221    }
222
223    /// Creates a 403 error with the given message.
224    pub fn forbidden(msg: impl Into<String>) -> Self {
225        Self::new(msg, None, 403)
226    }
227
228    /// Creates a 503 error with the given message.
229    pub fn service_unavailable(msg: impl Into<String>) -> Self {
230        Self::new(msg, None, 503)
231    }
232
233    /// Creates a 400 error with the given message.
234    pub fn bad_request(msg: impl Into<String>) -> Self {
235        Self::new(msg, None, 400)
236    }
237    /// Creates a 404 error with the given message.
238    pub fn not_found(msg: impl Into<String>) -> Self {
239        Self::new(msg, None, 404)
240    }
241    /// Creates a 500 error with the given message.
242    pub fn internal(msg: impl Into<String>) -> Self {
243        Self::new(msg, None, 500)
244    }
245    /// Creates an error from a displayable value with an explicit HTTP status `code`.
246    pub fn from_error<E: std::fmt::Display + ?Sized>(err: &E, code: u16) -> Self {
247        Self::new(err.to_string(), None, code)
248    }
249
250    /// Maps an `anyhow` error to a 400 when its message suggests bad input or a missing
251    /// resource (`validation`/`invalid`/`not found`), otherwise to a 500.
252    pub fn of(err: &anyhow::Error) -> Self {
253        let msg = err.to_string();
254        let lower = msg.to_lowercase();
255        if lower.contains("validation") || lower.contains("invalid") || lower.contains("not found")
256        {
257            Self::new(msg, None, 400)
258        } else {
259            Self::new(msg, None, 500)
260        }
261    }
262    /// Converts this error into an `"error"`-status `ServiceResult` for rendering.
263    pub fn to_service_result(&self) -> ServiceResult<Value> {
264        ServiceResult::new("error", self.message.clone(), self.data.clone(), self.code)
265    }
266    /// Returns the HTTP status code for this error.
267    pub fn code(&self) -> u16 {
268        self.code
269    }
270}
271
272impl std::fmt::Display for ErrorResult {
273    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
274        write!(f, "Error {}: {}", self.code, self.message)
275    }
276}
277impl std::error::Error for ErrorResult {}
278
279impl From<sea_orm::DbErr> for ErrorResult {
280    fn from(err: sea_orm::DbErr) -> Self {
281        match err {
282            sea_orm::DbErr::RecordNotFound(_) => Self::not_found("Could not find resource"),
283            sea_orm::DbErr::Custom(e) => Self::new(e, None, 503),
284            sea_orm::DbErr::RecordNotInserted => Self::service_unavailable("Record not inserted"),
285            sea_orm::DbErr::RbacError(_) => Self::forbidden("Access denied"),
286            sea_orm::DbErr::AccessDenied {
287                permission,
288                resource,
289            } => Self::forbidden(format!("Access denied: {} on {}", permission, resource)),
290            others => {
291                tracing::error!(target: "framework", "Database error: {:?}", others);
292                Self::internal("An unknown error has occurred. Please try again later")
293            },
294        }
295    }
296}
297
298impl TypedServiceResult for ErrorResult {
299    fn message(&self) -> &str {
300        &self.message
301    }
302    fn code(&self) -> u16 {
303        self.code
304    }
305    fn response_type(&self) -> ResponseType {
306        ResponseType::Json
307    }
308    fn serialize(&self) -> Result<String, serde_json::Error> {
309        self.to_service_result().serialize_inherent()
310    }
311}
312
313/// Renders any `TypedServiceResult` as an `http::Response<String>` with correlation
314/// headers (`X-Request-ID`, `X-Correlation-ID`, `X-Correlation-Flow`) and a content type
315/// derived from the result. Serialization failures fall back to the result message as body.
316pub fn build_response(
317    result: &dyn TypedServiceResult,
318    ctx: Arc<crate::logging::CorrelationContext>,
319) -> http::Response<String> {
320    let body = result
321        .serialize()
322        .unwrap_or_else(|_| result.message().to_string());
323    let mut builder = http::Response::builder()
324        .status(result.code())
325        .header("X-Request-ID", ctx.request_id())
326        .header("X-Correlation-ID", ctx.correlation_id())
327        .header("X-Correlation-Flow", ctx.flow().to_string());
328    let content_type = match result.response_type() {
329        ResponseType::Json => "application/json",
330        ResponseType::Html => "text/html",
331        ResponseType::Xml => "application/xml",
332        ResponseType::Javascript => "application/javascript",
333        ResponseType::File => "application/octet-stream",
334        ResponseType::Text => "text/plain",
335    };
336    builder = builder.header("Content-Type", content_type);
337    if result.response_type() == ResponseType::File {
338        builder = builder.header(
339            "Content-Disposition",
340            format!(
341                "attachment; filename=\"{}\"",
342                body.rsplit('/').next().unwrap_or("file")
343            ),
344        );
345    }
346    builder.body(body).unwrap()
347}
348
349/// Renders a `ServiceResult<T>` as an `http::Response<String>`; see [`build_response`].
350pub fn build_response_service<T: Serialize + Send + Sync>(
351    result: &ServiceResult<T>,
352    ctx: Arc<crate::logging::CorrelationContext>,
353) -> http::Response<String> {
354    build_response(result as &dyn TypedServiceResult, ctx)
355}
356
357/// Renders an `ErrorResult` as an `http::Response<String>` with an `"error"` envelope.
358pub fn error_response(
359    err: &ErrorResult,
360    ctx: Arc<crate::logging::CorrelationContext>,
361) -> http::Response<String> {
362    let sr = err.to_service_result();
363    build_response(&sr, ctx)
364}