ts_webapi/
error_response.rs

1//! A standardised error response from the API.
2
3use core::panic::Location;
4
5use bytes::Bytes;
6use http::{HeaderValue, Response, StatusCode, header};
7use serde::{Deserialize, Serialize};
8
9/// An error response from a route handler.
10#[derive(Clone, Debug)]
11#[non_exhaustive]
12pub enum ErrorResponse {
13    /// A bad request response.
14    BadRequest(BadRequest),
15    /// A not found response.
16    NotFound,
17    /// An internal server error response.
18    InternalServerError,
19}
20
21impl ErrorResponse {
22    /// Create a new not found response.
23    pub fn not_found() -> Self {
24        Self::NotFound
25    }
26
27    /// Create a new internal server error response.
28    pub fn internal_server_error() -> Self {
29        Self::InternalServerError
30    }
31
32    /// Convert the bad request into a response.
33    ///
34    /// ## Panics
35    /// * If a `BadRequest` has no problems.
36    /// * If the response built is invalid.
37    #[track_caller]
38    pub fn into_response<B: From<Bytes>>(self) -> Response<B> {
39        match self {
40            Self::BadRequest(bad_request) => bad_request.into_response(),
41            Self::NotFound => Response::builder()
42                .status(StatusCode::NOT_FOUND)
43                .body(B::from(Bytes::new()))
44                .expect("response should be valid"),
45            Self::InternalServerError => Response::builder()
46                .status(StatusCode::INTERNAL_SERVER_ERROR)
47                .body(B::from(Bytes::new()))
48                .expect("response should be valid"),
49        }
50    }
51}
52
53impl From<BadRequest> for ErrorResponse {
54    fn from(value: BadRequest) -> Self {
55        Self::BadRequest(value)
56    }
57}
58
59#[derive(Clone, Debug, Serialize, Deserialize)]
60#[serde(rename_all = "camelCase")]
61/// A problem detailing part of the error response.
62pub struct Problem {
63    /// A JSON path that identifies the part of the request that was the cause of the problem.
64    pub pointer: String,
65    /// A human-readable explanation specific to this occurrence of the problem.
66    pub detail: String,
67}
68impl Problem {
69    /// Create a new problem from a pointer and some details.
70    pub fn new<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
71        Self {
72            pointer: pointer.to_string(),
73            detail: detail.to_string(),
74        }
75    }
76}
77
78/// JSON payload for a bad request.
79#[derive(Clone, Debug, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct BadRequest {
82    /// The problems for the bad request.
83    pub problems: Vec<Problem>,
84}
85
86impl BadRequest {
87    /// A basic, single problem bad request.
88    pub fn basic<S1: ToString, S2: ToString>(pointer: S1, detail: S2) -> Self {
89        Self {
90            problems: vec![Problem::new(pointer, detail)],
91        }
92    }
93
94    /// An empty bad request.
95    pub fn new() -> Self {
96        Self { problems: vec![] }
97    }
98
99    /// Add a problem to the bad request.
100    pub fn add_problem(&mut self, problem: Problem) {
101        self.problems.push(problem);
102    }
103
104    /// If the
105    pub fn has_problems(&self) -> bool {
106        !self.problems.is_empty()
107    }
108
109    /// Convert the bad request into a response.
110    ///
111    /// ## Panics
112    /// * If the bad request has no problems
113    #[track_caller]
114    pub fn into_response<B: From<Bytes>>(self) -> Response<B> {
115        assert!(
116            self.has_problems(),
117            "{:?} An empty bad request cannot be returned",
118            Location::caller()
119        );
120
121        let json =
122            serde_json::to_string(&self).expect("error response should always be serializable");
123
124        Response::builder()
125            .status(StatusCode::BAD_REQUEST)
126            .header(
127                header::CONTENT_TYPE,
128                HeaderValue::from_static(mime::APPLICATION_JSON.as_ref()),
129            )
130            .body(B::from(Bytes::from(json)))
131            .expect("error response should produce a valid response")
132    }
133}
134
135#[cfg(feature = "axum")]
136impl axum::response::IntoResponse for BadRequest {
137    fn into_response(self) -> axum::response::Response {
138        self.into_response::<axum::body::Body>().into_response()
139    }
140}
141
142#[cfg(feature = "axum")]
143impl axum::response::IntoResponse for ErrorResponse {
144    fn into_response(self) -> axum::response::Response {
145        self.into_response()
146    }
147}