1use axum::{
2 http::StatusCode,
3 response::{IntoResponse, Response},
4 Json,
5};
6use meritocrab_core::CoreError;
7use meritocrab_db::DbError;
8use meritocrab_github::GithubError;
9use serde::{Deserialize, Serialize};
10use std::fmt;
11
12#[derive(Debug)]
14pub enum ApiError {
15 Database(DbError),
17
18 Github(GithubError),
20
21 Core(CoreError),
23
24 InvalidPayload(String),
26
27 InvalidSignature(String),
29
30 Internal(String),
32
33 Unauthorized(String),
35
36 NotFound(String),
38
39 BadRequest(String),
41
42 Forbidden(String),
44
45 InternalError(String),
47}
48
49impl fmt::Display for ApiError {
50 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51 match self {
52 ApiError::Database(e) => write!(f, "Database error: {}", e),
53 ApiError::Github(e) => write!(f, "GitHub error: {}", e),
54 ApiError::Core(e) => write!(f, "Core error: {}", e),
55 ApiError::InvalidPayload(msg) => write!(f, "Invalid payload: {}", msg),
56 ApiError::InvalidSignature(msg) => write!(f, "Invalid signature: {}", msg),
57 ApiError::Internal(msg) => write!(f, "Internal error: {}", msg),
58 ApiError::Unauthorized(msg) => write!(f, "Unauthorized: {}", msg),
59 ApiError::NotFound(msg) => write!(f, "Not found: {}", msg),
60 ApiError::BadRequest(msg) => write!(f, "Bad request: {}", msg),
61 ApiError::Forbidden(msg) => write!(f, "Forbidden: {}", msg),
62 ApiError::InternalError(msg) => write!(f, "Internal error: {}", msg),
63 }
64 }
65}
66
67impl std::error::Error for ApiError {}
68
69#[derive(Debug, Serialize, Deserialize)]
71pub struct ErrorResponse {
72 pub error: String,
73 pub message: String,
74}
75
76impl IntoResponse for ApiError {
77 fn into_response(self) -> Response {
78 let (status, error_type, message) = match &self {
79 ApiError::Database(e) => (
80 StatusCode::INTERNAL_SERVER_ERROR,
81 "database_error",
82 e.to_string(),
83 ),
84 ApiError::Github(e) => (
85 StatusCode::INTERNAL_SERVER_ERROR,
86 "github_error",
87 e.to_string(),
88 ),
89 ApiError::Core(e) => (
90 StatusCode::INTERNAL_SERVER_ERROR,
91 "core_error",
92 e.to_string(),
93 ),
94 ApiError::InvalidPayload(msg) => (
95 StatusCode::BAD_REQUEST,
96 "invalid_payload",
97 msg.clone(),
98 ),
99 ApiError::InvalidSignature(msg) => (
100 StatusCode::UNAUTHORIZED,
101 "invalid_signature",
102 msg.clone(),
103 ),
104 ApiError::Internal(msg) | ApiError::InternalError(msg) => (
105 StatusCode::INTERNAL_SERVER_ERROR,
106 "internal_error",
107 msg.clone(),
108 ),
109 ApiError::Unauthorized(msg) => (
110 StatusCode::UNAUTHORIZED,
111 "unauthorized",
112 msg.clone(),
113 ),
114 ApiError::NotFound(msg) => (
115 StatusCode::NOT_FOUND,
116 "not_found",
117 msg.clone(),
118 ),
119 ApiError::BadRequest(msg) => (
120 StatusCode::BAD_REQUEST,
121 "bad_request",
122 msg.clone(),
123 ),
124 ApiError::Forbidden(msg) => (
125 StatusCode::FORBIDDEN,
126 "forbidden",
127 msg.clone(),
128 ),
129 };
130
131 let error_response = ErrorResponse {
132 error: error_type.to_string(),
133 message,
134 };
135
136 (status, Json(error_response)).into_response()
137 }
138}
139
140impl From<DbError> for ApiError {
142 fn from(e: DbError) -> Self {
143 ApiError::Database(e)
144 }
145}
146
147impl From<GithubError> for ApiError {
148 fn from(e: GithubError) -> Self {
149 ApiError::Github(e)
150 }
151}
152
153impl From<CoreError> for ApiError {
154 fn from(e: CoreError) -> Self {
155 ApiError::Core(e)
156 }
157}
158
159impl From<serde_json::Error> for ApiError {
160 fn from(e: serde_json::Error) -> Self {
161 ApiError::InvalidPayload(format!("JSON parsing error: {}", e))
162 }
163}
164
165pub type ApiResult<T> = Result<T, ApiError>;
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170
171 #[test]
172 fn test_error_display() {
173 let err = ApiError::InvalidPayload("test error".to_string());
174 assert_eq!(err.to_string(), "Invalid payload: test error");
175 }
176
177 #[test]
178 fn test_error_response_invalid_payload() {
179 let err = ApiError::InvalidPayload("bad json".to_string());
180 let response = err.into_response();
181 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
182 }
183
184 #[test]
185 fn test_error_response_invalid_signature() {
186 let err = ApiError::InvalidSignature("signature mismatch".to_string());
187 let response = err.into_response();
188 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
189 }
190
191 #[test]
192 fn test_error_response_internal() {
193 let err = ApiError::Internal("something went wrong".to_string());
194 let response = err.into_response();
195 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
196 }
197
198 #[test]
199 fn test_from_serde_json_error() {
200 let json_err = serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
201 let api_err: ApiError = json_err.into();
202 match api_err {
203 ApiError::InvalidPayload(msg) => assert!(msg.contains("JSON parsing error")),
204 _ => panic!("Expected InvalidPayload error"),
205 }
206 }
207}