whynot_errors/
lib.rs

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