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