1use axum::{
2 Json,
3 http::StatusCode,
4 response::{IntoResponse, Response},
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, "invalid_payload", msg.clone())
96 }
97 ApiError::InvalidSignature(msg) => {
98 (StatusCode::UNAUTHORIZED, "invalid_signature", msg.clone())
99 }
100 ApiError::Internal(msg) | ApiError::InternalError(msg) => (
101 StatusCode::INTERNAL_SERVER_ERROR,
102 "internal_error",
103 msg.clone(),
104 ),
105 ApiError::Unauthorized(msg) => (StatusCode::UNAUTHORIZED, "unauthorized", msg.clone()),
106 ApiError::NotFound(msg) => (StatusCode::NOT_FOUND, "not_found", msg.clone()),
107 ApiError::BadRequest(msg) => (StatusCode::BAD_REQUEST, "bad_request", msg.clone()),
108 ApiError::Forbidden(msg) => (StatusCode::FORBIDDEN, "forbidden", msg.clone()),
109 };
110
111 let error_response = ErrorResponse {
112 error: error_type.to_string(),
113 message,
114 };
115
116 (status, Json(error_response)).into_response()
117 }
118}
119
120impl From<DbError> for ApiError {
122 fn from(e: DbError) -> Self {
123 ApiError::Database(e)
124 }
125}
126
127impl From<GithubError> for ApiError {
128 fn from(e: GithubError) -> Self {
129 ApiError::Github(e)
130 }
131}
132
133impl From<CoreError> for ApiError {
134 fn from(e: CoreError) -> Self {
135 ApiError::Core(e)
136 }
137}
138
139impl From<serde_json::Error> for ApiError {
140 fn from(e: serde_json::Error) -> Self {
141 ApiError::InvalidPayload(format!("JSON parsing error: {}", e))
142 }
143}
144
145pub type ApiResult<T> = Result<T, ApiError>;
146
147#[cfg(test)]
148mod tests {
149 use super::*;
150
151 #[test]
152 fn test_error_display() {
153 let err = ApiError::InvalidPayload("test error".to_string());
154 assert_eq!(err.to_string(), "Invalid payload: test error");
155 }
156
157 #[test]
158 fn test_error_response_invalid_payload() {
159 let err = ApiError::InvalidPayload("bad json".to_string());
160 let response = err.into_response();
161 assert_eq!(response.status(), StatusCode::BAD_REQUEST);
162 }
163
164 #[test]
165 fn test_error_response_invalid_signature() {
166 let err = ApiError::InvalidSignature("signature mismatch".to_string());
167 let response = err.into_response();
168 assert_eq!(response.status(), StatusCode::UNAUTHORIZED);
169 }
170
171 #[test]
172 fn test_error_response_internal() {
173 let err = ApiError::Internal("something went wrong".to_string());
174 let response = err.into_response();
175 assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
176 }
177
178 #[test]
179 fn test_from_serde_json_error() {
180 let json_err = serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
181 let api_err: ApiError = json_err.into();
182 match api_err {
183 ApiError::InvalidPayload(msg) => assert!(msg.contains("JSON parsing error")),
184 _ => panic!("Expected InvalidPayload error"),
185 }
186 }
187}