Skip to main content

tuitbot_server/
error.rs

1//! API error types for the tuitbot server.
2//!
3//! Maps core domain errors to HTTP status codes and JSON error responses.
4
5use axum::http::StatusCode;
6use axum::response::{IntoResponse, Response};
7use serde_json::json;
8
9/// API error type for route handlers.
10pub enum ApiError {
11    /// Internal storage/database error.
12    Storage(tuitbot_core::error::StorageError),
13    /// Requested resource not found.
14    NotFound(String),
15    /// Bad request (invalid query parameters, etc.).
16    BadRequest(String),
17    /// Conflict (resource already exists, runtime already running, etc.).
18    Conflict(String),
19    /// Internal server error (non-storage).
20    Internal(String),
21    /// Forbidden — insufficient role/permissions.
22    Forbidden(String),
23}
24
25impl From<tuitbot_core::error::StorageError> for ApiError {
26    fn from(err: tuitbot_core::error::StorageError) -> Self {
27        match err {
28            tuitbot_core::error::StorageError::AlreadyReviewed { id, current_status } => {
29                Self::Conflict(format!(
30                    "item {id} has already been reviewed (current status: {current_status})"
31                ))
32            }
33            other => Self::Storage(other),
34        }
35    }
36}
37
38impl From<crate::account::AccountError> for ApiError {
39    fn from(err: crate::account::AccountError) -> Self {
40        match err.status {
41            StatusCode::FORBIDDEN => Self::Forbidden(err.message),
42            StatusCode::NOT_FOUND => Self::NotFound(err.message),
43            _ => Self::Internal(err.message),
44        }
45    }
46}
47
48impl IntoResponse for ApiError {
49    fn into_response(self) -> Response {
50        let (status, message) = match self {
51            Self::Storage(e) => {
52                tracing::error!("storage error: {e}");
53                (StatusCode::INTERNAL_SERVER_ERROR, e.to_string())
54            }
55            Self::NotFound(msg) => (StatusCode::NOT_FOUND, msg),
56            Self::BadRequest(msg) => (StatusCode::BAD_REQUEST, msg),
57            Self::Conflict(msg) => (StatusCode::CONFLICT, msg),
58            Self::Internal(msg) => {
59                tracing::error!("internal error: {msg}");
60                (StatusCode::INTERNAL_SERVER_ERROR, msg)
61            }
62            Self::Forbidden(msg) => (StatusCode::FORBIDDEN, msg),
63        };
64
65        let body = axum::Json(json!({ "error": message }));
66        (status, body).into_response()
67    }
68}