Skip to main content

meritocrab_api/
error.rs

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/// API error type
13#[derive(Debug)]
14pub enum ApiError {
15    /// Database error
16    Database(DbError),
17
18    /// GitHub API error
19    Github(GithubError),
20
21    /// Core logic error
22    Core(CoreError),
23
24    /// Invalid request payload
25    InvalidPayload(String),
26
27    /// Invalid webhook signature (HMAC verification failed)
28    InvalidSignature(String),
29
30    /// Internal server error
31    Internal(String),
32
33    /// Unauthorized (401)
34    Unauthorized(String),
35
36    /// Not found (404)
37    NotFound(String),
38
39    /// Bad request (400)
40    BadRequest(String),
41
42    /// Forbidden (403)
43    Forbidden(String),
44
45    /// Internal error (alias for backward compatibility)
46    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/// Error response JSON structure
70#[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
140// Conversions from domain errors to ApiError
141impl 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}