1use axum::http::StatusCode;
46use axum::response::{IntoResponse, Response};
47use serde::Serialize;
48
49#[derive(Debug)]
54struct StringError(String);
55
56impl std::fmt::Display for StringError {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 f.write_str(&self.0)
59 }
60}
61
62impl std::error::Error for StringError {}
63
64#[derive(Serialize)]
66struct ErrorBody {
67 error: ErrorInner,
68}
69
70#[derive(Serialize)]
71struct ErrorInner {
72 status: u16,
73 message: String,
74}
75
76pub struct AutumnError {
116 inner: Box<dyn std::error::Error + Send + Sync>,
117 status: StatusCode,
118}
119
120pub type AutumnResult<T> = Result<T, AutumnError>;
136
137impl<E> From<E> for AutumnError
138where
139 E: std::error::Error + Send + Sync + 'static,
140{
141 fn from(err: E) -> Self {
142 Self {
143 inner: Box::new(err),
144 status: StatusCode::INTERNAL_SERVER_ERROR,
145 }
146 }
147}
148
149impl AutumnError {
150 #[must_use]
163 pub const fn with_status(mut self, status: StatusCode) -> Self {
164 self.status = status;
165 self
166 }
167
168 pub fn not_found(err: impl std::error::Error + Send + Sync + 'static) -> Self {
180 Self {
181 inner: Box::new(err),
182 status: StatusCode::NOT_FOUND,
183 }
184 }
185
186 pub fn bad_request(err: impl std::error::Error + Send + Sync + 'static) -> Self {
198 Self {
199 inner: Box::new(err),
200 status: StatusCode::BAD_REQUEST,
201 }
202 }
203
204 pub fn unprocessable(err: impl std::error::Error + Send + Sync + 'static) -> Self {
219 Self {
220 inner: Box::new(err),
221 status: StatusCode::UNPROCESSABLE_ENTITY,
222 }
223 }
224
225 pub fn service_unavailable(err: impl std::error::Error + Send + Sync + 'static) -> Self {
237 Self {
238 inner: Box::new(err),
239 status: StatusCode::SERVICE_UNAVAILABLE,
240 }
241 }
242
243 pub fn not_found_msg(msg: impl Into<String>) -> Self {
247 Self::not_found(StringError(msg.into()))
248 }
249
250 pub fn bad_request_msg(msg: impl Into<String>) -> Self {
252 Self::bad_request(StringError(msg.into()))
253 }
254
255 pub fn unprocessable_msg(msg: impl Into<String>) -> Self {
257 Self::unprocessable(StringError(msg.into()))
258 }
259
260 pub fn service_unavailable_msg(msg: impl Into<String>) -> Self {
262 Self::service_unavailable(StringError(msg.into()))
263 }
264
265 #[must_use]
277 pub const fn status(&self) -> StatusCode {
278 self.status
279 }
280}
281
282impl std::fmt::Display for AutumnError {
283 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
284 write!(f, "{}", self.inner)
285 }
286}
287
288impl std::fmt::Debug for AutumnError {
289 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
290 f.debug_struct("AutumnError")
291 .field("status", &self.status)
292 .field("inner", &self.inner)
293 .finish()
294 }
295}
296
297impl IntoResponse for AutumnError {
298 fn into_response(self) -> Response {
299 let status = self.status;
300 let body = ErrorBody {
301 error: ErrorInner {
302 status: status.as_u16(),
303 message: self.inner.to_string(),
304 },
305 };
306
307 (status, axum::Json(body)).into_response()
308 }
309}
310
311#[cfg(test)]
312mod tests {
313 use super::*;
314 use axum::http::StatusCode;
315
316 #[derive(Debug)]
317 struct TestError(String);
318
319 impl std::fmt::Display for TestError {
320 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
321 write!(f, "{}", self.0)
322 }
323 }
324
325 impl std::error::Error for TestError {}
326
327 #[test]
328 fn blanket_from_defaults_to_500() {
329 let err: AutumnError = TestError("boom".into()).into();
330 assert_eq!(err.status(), StatusCode::INTERNAL_SERVER_ERROR);
331 }
332
333 #[test]
334 fn not_found_is_404() {
335 let err = AutumnError::not_found(TestError("missing".into()));
336 assert_eq!(err.status(), StatusCode::NOT_FOUND);
337 }
338
339 #[test]
340 fn bad_request_is_400() {
341 let err = AutumnError::bad_request(TestError("invalid input".into()));
342 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
343 }
344
345 #[test]
346 fn unprocessable_is_422() {
347 let err = AutumnError::unprocessable(TestError("bad entity".into()));
348 assert_eq!(err.status(), StatusCode::UNPROCESSABLE_ENTITY);
349 }
350
351 #[test]
352 fn service_unavailable_is_503() {
353 let err = AutumnError::service_unavailable(TestError("pool exhausted".into()));
354 assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE);
355 }
356
357 #[test]
358 fn not_found_msg_is_404() {
359 let err = AutumnError::not_found_msg("no such user");
360 assert_eq!(err.status(), StatusCode::NOT_FOUND);
361 assert_eq!(err.to_string(), "no such user");
362 }
363
364 #[test]
365 fn bad_request_msg_is_400() {
366 let err = AutumnError::bad_request_msg("invalid input");
367 assert_eq!(err.status(), StatusCode::BAD_REQUEST);
368 }
369
370 #[test]
371 fn unprocessable_msg_is_422() {
372 let err = AutumnError::unprocessable_msg("title required");
373 assert_eq!(err.status(), StatusCode::UNPROCESSABLE_ENTITY);
374 }
375
376 #[test]
377 fn service_unavailable_msg_is_503() {
378 let err = AutumnError::service_unavailable_msg("db down");
379 assert_eq!(err.status(), StatusCode::SERVICE_UNAVAILABLE);
380 assert_eq!(err.to_string(), "db down");
381 }
382
383 #[test]
384 fn with_status_overrides() {
385 let err: AutumnError = TestError("forbidden".into()).into();
386 let err = err.with_status(StatusCode::FORBIDDEN);
387 assert_eq!(err.status(), StatusCode::FORBIDDEN);
388 }
389
390 #[test]
391 fn display_uses_inner_message() {
392 let err: AutumnError = TestError("something broke".into()).into();
393 assert_eq!(err.to_string(), "something broke");
394 }
395
396 #[test]
397 fn into_response_has_correct_status() {
398 let err = AutumnError::not_found(TestError("not found".into()));
399 let response = err.into_response();
400 assert_eq!(response.status(), StatusCode::NOT_FOUND);
401 }
402
403 #[tokio::test]
404 async fn into_response_has_json_body() {
405 let err = AutumnError::not_found(TestError("not found".into()));
406 let response = err.into_response();
407
408 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
409 .await
410 .unwrap();
411 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
412
413 assert_eq!(json["error"]["status"], 404);
414 assert_eq!(json["error"]["message"], "not found");
415 }
416
417 #[test]
418 fn debug_shows_status_and_inner() {
419 let err = AutumnError::bad_request(TestError("oops".into()));
420 let debug = format!("{err:?}");
421 assert!(debug.contains("AutumnError"));
422 assert!(debug.contains("400"));
423 }
424
425 #[tokio::test]
426 async fn msg_constructor_produces_valid_json_response() {
427 let err = AutumnError::unprocessable_msg("title required");
428 let response = err.into_response();
429
430 assert_eq!(response.status(), StatusCode::UNPROCESSABLE_ENTITY);
431 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
432 .await
433 .unwrap();
434 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
435 assert_eq!(json["error"]["status"], 422);
436 assert_eq!(json["error"]["message"], "title required");
437 }
438
439 #[tokio::test]
440 async fn service_unavailable_response_is_503() {
441 let err = AutumnError::service_unavailable_msg("db down");
442 let response = err.into_response();
443
444 assert_eq!(response.status(), StatusCode::SERVICE_UNAVAILABLE);
445 let body = axum::body::to_bytes(response.into_body(), usize::MAX)
446 .await
447 .unwrap();
448 let json: serde_json::Value = serde_json::from_slice(&body).unwrap();
449 assert_eq!(json["error"]["status"], 503);
450 assert_eq!(json["error"]["message"], "db down");
451 }
452}