1use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use serde::Serialize;
6
7pub type AppResult<T> = Result<T, AppError>;
9
10#[derive(Debug, Clone)]
12pub enum AppError {
13 BadRequest(String),
14 Unauthorized(String),
15 Forbidden(String),
16 NotFound(String),
17 Conflict(String),
18 Internal(String),
19}
20
21impl AppError {
22 pub fn bad_request(msg: impl Into<String>) -> Self {
23 Self::BadRequest(msg.into())
24 }
25
26 pub fn not_found(msg: impl Into<String>) -> Self {
27 Self::NotFound(msg.into())
28 }
29
30 pub fn conflict(msg: impl Into<String>) -> Self {
31 Self::Conflict(msg.into())
32 }
33
34 pub fn internal(msg: impl Into<String>) -> Self {
35 Self::Internal(msg.into())
36 }
37
38 pub fn unauthorized(msg: impl Into<String>) -> Self {
39 Self::Unauthorized(msg.into())
40 }
41
42 pub fn forbidden(msg: impl Into<String>) -> Self {
43 Self::Forbidden(msg.into())
44 }
45
46 pub fn status(&self) -> StatusCode {
47 match self {
48 AppError::BadRequest(_) => StatusCode::BAD_REQUEST,
49 AppError::Unauthorized(_) => StatusCode::UNAUTHORIZED,
50 AppError::Forbidden(_) => StatusCode::FORBIDDEN,
51 AppError::NotFound(_) => StatusCode::NOT_FOUND,
52 AppError::Conflict(_) => StatusCode::CONFLICT,
53 AppError::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
54 }
55 }
56}
57
58#[derive(Serialize)]
59struct ErrorBody {
60 status: u16,
61 message: String,
62}
63
64impl std::fmt::Display for AppError {
65 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
66 match self {
67 AppError::BadRequest(m) => write!(f, "[Ruest] Bad Request: {m}"),
68 AppError::Unauthorized(m) => write!(f, "[Ruest] Unauthorized: {m}"),
69 AppError::Forbidden(m) => write!(f, "[Ruest] Forbidden: {m}"),
70 AppError::NotFound(m) => write!(f, "[Ruest] Not Found: {m}"),
71 AppError::Conflict(m) => write!(f, "[Ruest] Conflict: {m}"),
72 AppError::Internal(m) => write!(f, "[Ruest] Internal Error: {m}"),
73 }
74 }
75}
76
77impl std::error::Error for AppError {}
78
79impl IntoResponse for AppError {
80 fn into_response(self) -> Response {
81 let status = self.status();
82 let message = match &self {
83 AppError::BadRequest(m)
84 | AppError::Unauthorized(m)
85 | AppError::Forbidden(m)
86 | AppError::NotFound(m)
87 | AppError::Conflict(m)
88 | AppError::Internal(m) => m.clone(),
89 };
90 let body = ErrorBody {
91 status: status.as_u16(),
92 message,
93 };
94 (status, axum::Json(body)).into_response()
95 }
96}
97
98#[cfg(test)]
99mod tests {
100 use super::*;
101 use axum::http::StatusCode;
102
103 #[test]
104 fn status_codes_match_http_semantics() {
105 assert_eq!(
106 AppError::bad_request("x").status(),
107 StatusCode::BAD_REQUEST
108 );
109 assert_eq!(AppError::not_found("x").status(), StatusCode::NOT_FOUND);
110 assert_eq!(AppError::conflict("x").status(), StatusCode::CONFLICT);
111 assert_eq!(
112 AppError::internal("x").status(),
113 StatusCode::INTERNAL_SERVER_ERROR
114 );
115 }
116
117 #[test]
118 fn display_is_human_readable() {
119 let msg = AppError::not_found("missing").to_string();
120 assert!(msg.contains("Not Found"));
121 assert!(msg.contains("missing"));
122 }
123}