1use http::StatusCode;
8use serde::Serialize;
9
10use crate::body::{body_from_bytes, body_from_string, BoxBody};
11use crate::response::IntoResponse;
12
13#[derive(Debug, Clone)]
32pub struct JsonError {
33 pub status: StatusCode,
34 pub message: String,
35}
36
37#[derive(Serialize)]
38struct JsonErrorBody {
39 error: JsonErrorInner,
40}
41
42#[derive(Serialize)]
43struct JsonErrorInner {
44 status: u16,
45 message: String,
46}
47
48impl JsonError {
49 pub fn new(status: StatusCode, message: impl Into<String>) -> Self {
51 JsonError {
52 status,
53 message: message.into(),
54 }
55 }
56
57 pub fn bad_request(message: impl Into<String>) -> Self {
59 Self::new(StatusCode::BAD_REQUEST, message)
60 }
61
62 pub fn unauthorized(message: impl Into<String>) -> Self {
64 Self::new(StatusCode::UNAUTHORIZED, message)
65 }
66
67 pub fn forbidden(message: impl Into<String>) -> Self {
69 Self::new(StatusCode::FORBIDDEN, message)
70 }
71
72 pub fn not_found(message: impl Into<String>) -> Self {
74 Self::new(StatusCode::NOT_FOUND, message)
75 }
76
77 pub fn conflict(message: impl Into<String>) -> Self {
79 Self::new(StatusCode::CONFLICT, message)
80 }
81
82 pub fn unprocessable(message: impl Into<String>) -> Self {
84 Self::new(StatusCode::UNPROCESSABLE_ENTITY, message)
85 }
86
87 pub fn internal(message: impl Into<String>) -> Self {
89 Self::new(StatusCode::INTERNAL_SERVER_ERROR, message)
90 }
91}
92
93impl IntoResponse for JsonError {
94 fn into_response(self) -> http::Response<BoxBody> {
95 let body = JsonErrorBody {
96 error: JsonErrorInner {
97 status: self.status.as_u16(),
98 message: self.message,
99 },
100 };
101 match serde_json::to_vec(&body) {
102 Ok(bytes) => {
103 let body = body_from_bytes(bytes::Bytes::from(bytes));
104 let mut res = http::Response::new(body);
105 *res.status_mut() = self.status;
106 res.headers_mut().insert(
107 http::header::CONTENT_TYPE,
108 http::HeaderValue::from_static("application/json"),
109 );
110 res
111 }
112 Err(e) => {
113 let mut res = http::Response::new(body_from_string(format!(
114 "error serialization failed: {e}"
115 )));
116 *res.status_mut() = StatusCode::INTERNAL_SERVER_ERROR;
117 res
118 }
119 }
120 }
121}
122
123impl std::fmt::Display for JsonError {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(
126 f,
127 "{} {}: {}",
128 self.status.as_u16(),
129 self.status,
130 self.message
131 )
132 }
133}
134
135impl std::error::Error for JsonError {}
136
137impl From<(StatusCode, String)> for JsonError {
140 fn from((status, message): (StatusCode, String)) -> Self {
141 JsonError { status, message }
142 }
143}
144
145#[cfg(feature = "openapi")]
150impl typeway_openapi::ErrorResponses for JsonError {
151 fn error_responses() -> indexmap::IndexMap<String, typeway_openapi::spec::Response> {
152 use typeway_openapi::spec::*;
153
154 let mut content = indexmap::IndexMap::new();
155 let mut properties = indexmap::IndexMap::new();
156
157 let mut error_props = indexmap::IndexMap::new();
158 error_props.insert("status".to_string(), Schema::integer());
159 error_props.insert("message".to_string(), Schema::string());
160
161 properties.insert(
162 "error".to_string(),
163 Schema {
164 schema_type: Some("object".into()),
165 properties: Some(error_props),
166 ..Default::default()
167 },
168 );
169
170 content.insert(
171 "application/json".to_string(),
172 MediaType {
173 schema: Some(Schema {
174 schema_type: Some("object".into()),
175 properties: Some(properties),
176 description: Some("JSON error response".into()),
177 ..Default::default()
178 }),
179 example: None,
180 },
181 );
182
183 let mut responses = indexmap::IndexMap::new();
184 responses.insert(
185 "4XX".to_string(),
186 Response {
187 description: "Client error".to_string(),
188 content: content.clone(),
189 },
190 );
191 responses.insert(
192 "5XX".to_string(),
193 Response {
194 description: "Server error".to_string(),
195 content,
196 },
197 );
198 responses
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use super::*;
205
206 #[test]
207 fn json_error_response() {
208 let err = JsonError::not_found("user not found");
209 let res = err.into_response();
210 assert_eq!(res.status(), StatusCode::NOT_FOUND);
211 assert_eq!(
212 res.headers().get("content-type").unwrap(),
213 "application/json"
214 );
215 }
216
217 #[test]
218 fn json_error_from_tuple() {
219 let err: JsonError = (StatusCode::BAD_REQUEST, "bad input".to_string()).into();
220 assert_eq!(err.status, StatusCode::BAD_REQUEST);
221 assert_eq!(err.message, "bad input");
222 }
223}