Skip to main content

ruest/http/
result.rs

1//! Résultats et erreurs HTTP lisibles (`AppResult`, pas de `Result<Result<...>>`).
2
3use axum::http::StatusCode;
4use axum::response::{IntoResponse, Response};
5use serde::Serialize;
6
7/// Résultat standard d’un handler ou service HTTP.
8pub type AppResult<T> = Result<T, AppError>;
9
10/// Erreurs métier avec messages humains (évite les messages Rust cryptiques côté API).
11#[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}