whynot_errors/
app_error.rs

1use std::fmt::Display;
2
3use axum::http::StatusCode;
4use axum::response::{Html, IntoResponse, Response};
5use axum::Json;
6use tracing::{error, warn};
7
8/// Global error type
9/// Use in basically all scenarios where an error is needed.
10#[derive(Debug)]
11pub struct AppError {
12    pub code: StatusCode,
13    pub message: String,
14}
15
16impl Display for AppError {
17    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
18        write!(f, "Code: {}; {};", self.code.as_u16(), self.message)
19    }
20}
21
22impl AppError {
23    /// Create a new `AppError` from any `ToString` with a code 500.
24    /// If you want to customize the code, use the `AppError::code` factory.
25    pub fn new(obj: impl ToString) -> Self {
26        error!("Server Error {}", obj.to_string());
27
28        Self {
29            code: StatusCode::INTERNAL_SERVER_ERROR,
30            message: obj.to_string(),
31        }
32    }
33
34    /// FIXME: Remove this prior to version 1
35    #[deprecated]
36    pub fn from(obj: impl ToString) -> Self {
37        Self {
38            code: StatusCode::INTERNAL_SERVER_ERROR,
39            message: obj.to_string(),
40        }
41    }
42
43    /// Return a closure which will accept a ToString to generate an AppError
44    pub fn code<T: ToString>(code: StatusCode) -> impl Fn(T) -> Self {
45        move |obj| {
46            warn!(code = code.as_u16(), message = obj.to_string(), "Error");
47
48            Self {
49                code,
50                message: obj.to_string(),
51            }
52        }
53    }
54}
55
56impl IntoResponse for AppError {
57    fn into_response(self) -> Response {
58        (self.code, self.message).into_response()
59    }
60}
61
62/// Use this for most functions that return a result
63pub type AppResult<T> = Result<T, AppError>;
64
65/// If you are returning JSON, use this.
66pub type JsonResult<T> = AppResult<Json<T>>;
67
68/// Shortcut to wrap a result in json. Will consume the input.
69pub fn json_ok<T>(obj: T) -> JsonResult<T> {
70    Ok(Json(obj))
71}
72
73/// If you are returning HTML, use this.
74pub type HtmlResult = AppResult<Html<String>>;
75
76/// Shortcut to wrap a result in html. Will consume the input.
77pub fn html_ok(s: impl ToString) -> HtmlResult {
78    Ok(Html(s.to_string()))
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_fmt() {
87        let err = AppError {
88            code: StatusCode::OK,
89            message: "ok".to_string(),
90        };
91
92        assert_eq!(err.to_string(), "Code: 200; ok;");
93    }
94
95    /// Test the from method. It should make an error from any object that implements `Display`
96    #[test]
97    fn test_from() {
98        let err2: AppError = AppError::new("hi");
99
100        assert_eq!(err2.message, "hi");
101        assert_eq!(err2.code, StatusCode::INTERNAL_SERVER_ERROR);
102    }
103
104    /// Test that the types are all correct for `json_ok`.
105    #[test]
106    fn test_json() {
107        let resp: JsonResult<String> = json_ok("hi".to_string());
108        assert_eq!(resp.unwrap().to_string(), "hi");
109    }
110
111    #[test]
112    fn test_traits() {
113        assert_eq!(AppError::new("hi").message, "hi");
114        assert_eq!(AppError::new("hi".to_string()).message, "hi");
115    }
116
117    #[test]
118    fn test_code() {
119        let r: Result<(), String> = Err("hi".to_string());
120        let mapped = r.map_err(AppError::code(StatusCode::METHOD_NOT_ALLOWED));
121
122        assert!(mapped.is_err());
123
124        let e = mapped.unwrap_err();
125
126        assert_eq!(e.code, StatusCode::METHOD_NOT_ALLOWED);
127        assert_eq!(e.message, "hi");
128    }
129}