Skip to main content

px_errors/
lib.rs

1use axum::Json;
2use axum::http::StatusCode;
3use axum::response::{IntoResponse, Response};
4use serde::Serialize;
5use thiserror::Error;
6
7#[derive(Debug, Error)]
8#[non_exhaustive]
9pub enum AppError {
10    #[error("not found: {0}")]
11    NotFound(String),
12    #[error("bad request: {0}")]
13    BadRequest(String),
14    #[error("validation error: {0}")]
15    ValidationError(String),
16    #[error("unauthorized: {0}")]
17    Unauthorized(String),
18    #[error("forbidden: {0}")]
19    Forbidden(String),
20    #[error("conflict: {0}")]
21    Conflict(String),
22    #[error("not implemented: {0}")]
23    NotImplemented(String),
24    #[error("internal error: {0}")]
25    InternalError(String),
26}
27
28#[derive(Debug, Serialize)]
29pub struct ErrorResponse {
30    pub error: &'static str,
31    pub message: String,
32}
33
34impl AppError {
35    pub fn kind(&self) -> &'static str {
36        match self {
37            Self::NotFound(_) => "not_found",
38            Self::BadRequest(_) => "bad_request",
39            Self::ValidationError(_) => "validation_error",
40            Self::Unauthorized(_) => "unauthorized",
41            Self::Forbidden(_) => "forbidden",
42            Self::Conflict(_) => "conflict",
43            Self::NotImplemented(_) => "not_implemented",
44            Self::InternalError(_) => "internal_error",
45        }
46    }
47
48    fn status(&self) -> StatusCode {
49        match self {
50            Self::NotFound(_) => StatusCode::NOT_FOUND,
51            Self::BadRequest(_) | Self::ValidationError(_) => StatusCode::BAD_REQUEST,
52            Self::Unauthorized(_) => StatusCode::UNAUTHORIZED,
53            Self::Forbidden(_) => StatusCode::FORBIDDEN,
54            Self::Conflict(_) => StatusCode::CONFLICT,
55            Self::NotImplemented(_) => StatusCode::NOT_IMPLEMENTED,
56            Self::InternalError(_) => StatusCode::INTERNAL_SERVER_ERROR,
57        }
58    }
59
60    fn message(&self) -> String {
61        match self {
62            Self::NotFound(m)
63            | Self::BadRequest(m)
64            | Self::ValidationError(m)
65            | Self::Unauthorized(m)
66            | Self::Forbidden(m)
67            | Self::Conflict(m)
68            | Self::NotImplemented(m)
69            | Self::InternalError(m) => m.clone(),
70        }
71    }
72}
73
74impl IntoResponse for AppError {
75    fn into_response(self) -> Response {
76        let status = self.status();
77        let body = ErrorResponse {
78            error: self.kind(),
79            message: self.message(),
80        };
81        (status, Json(body)).into_response()
82    }
83}
84
85#[cfg(test)]
86#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
87mod tests {
88    use super::*;
89
90    #[test]
91    fn status_codes_map_correctly() {
92        assert_eq!(
93            AppError::NotFound("x".into()).status(),
94            StatusCode::NOT_FOUND
95        );
96        assert_eq!(
97            AppError::BadRequest("x".into()).status(),
98            StatusCode::BAD_REQUEST
99        );
100        assert_eq!(
101            AppError::ValidationError("x".into()).status(),
102            StatusCode::BAD_REQUEST
103        );
104        assert_eq!(
105            AppError::Unauthorized("x".into()).status(),
106            StatusCode::UNAUTHORIZED
107        );
108        assert_eq!(
109            AppError::Forbidden("x".into()).status(),
110            StatusCode::FORBIDDEN
111        );
112        assert_eq!(
113            AppError::Conflict("x".into()).status(),
114            StatusCode::CONFLICT
115        );
116        assert_eq!(
117            AppError::NotImplemented("x".into()).status(),
118            StatusCode::NOT_IMPLEMENTED
119        );
120        assert_eq!(
121            AppError::InternalError("x".into()).status(),
122            StatusCode::INTERNAL_SERVER_ERROR
123        );
124    }
125
126    #[test]
127    fn kind_strings_are_stable() {
128        assert_eq!(AppError::NotFound("x".into()).kind(), "not_found");
129        assert_eq!(
130            AppError::NotImplemented("x".into()).kind(),
131            "not_implemented"
132        );
133    }
134
135    #[test]
136    fn message_round_trip() {
137        let m = "boom";
138        let err = AppError::InternalError(m.into());
139        assert_eq!(err.message(), m);
140        assert!(format!("{err}").contains(m));
141    }
142}