1use std::fmt;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ErrorKind {
24 BadRequest,
26 Unauthenticated,
28 Unauthorized,
30 NotFound,
32 MethodNotAllowed,
34 Conflict,
36 PayloadTooLarge,
38 Validation,
40 TooManyRequests,
42 Internal,
44 ServiceUnavailable,
46 Status(u16),
48}
49
50impl ErrorKind {
51 pub fn status(self) -> u16 {
53 match self {
54 ErrorKind::BadRequest => 400,
55 ErrorKind::Unauthenticated => 401,
56 ErrorKind::Unauthorized => 403,
57 ErrorKind::NotFound => 404,
58 ErrorKind::MethodNotAllowed => 405,
59 ErrorKind::Conflict => 409,
60 ErrorKind::PayloadTooLarge => 413,
61 ErrorKind::Validation => 422,
62 ErrorKind::TooManyRequests => 429,
63 ErrorKind::Internal => 500,
64 ErrorKind::ServiceUnavailable => 503,
65 ErrorKind::Status(code) => code,
66 }
67 }
68
69 pub fn is_server_error(self) -> bool {
72 self.status() >= 500
73 }
74}
75
76#[derive(Debug)]
79pub struct Error {
80 kind: ErrorKind,
81 message: String,
82 details: Option<serde_json::Value>,
83 source: Option<anyhow::Error>,
84}
85
86impl Error {
87 pub fn new(kind: ErrorKind, message: impl Into<String>) -> Self {
89 Self { kind, message: message.into(), details: None, source: None }
90 }
91
92 pub fn internal(message: impl Into<String>) -> Self {
94 Self::new(ErrorKind::Internal, message)
95 }
96
97 pub fn bad_request(message: impl Into<String>) -> Self {
99 Self::new(ErrorKind::BadRequest, message)
100 }
101
102 pub fn unauthenticated(message: impl Into<String>) -> Self {
104 Self::new(ErrorKind::Unauthenticated, message)
105 }
106
107 pub fn unauthorized(message: impl Into<String>) -> Self {
109 Self::new(ErrorKind::Unauthorized, message)
110 }
111
112 pub fn not_found(message: impl Into<String>) -> Self {
114 Self::new(ErrorKind::NotFound, message)
115 }
116
117 pub fn conflict(message: impl Into<String>) -> Self {
119 Self::new(ErrorKind::Conflict, message)
120 }
121
122 pub fn request_timeout(message: impl Into<String>) -> Self {
128 Self::new(ErrorKind::Status(408), message)
129 }
130
131 pub fn too_many_requests(message: impl Into<String>) -> Self {
136 Self::new(ErrorKind::TooManyRequests, message)
137 }
138
139 pub fn service_unavailable(message: impl Into<String>) -> Self {
145 Self::new(ErrorKind::ServiceUnavailable, message)
146 }
147
148 pub fn validation(details: serde_json::Value) -> Self {
152 Self::new(ErrorKind::Validation, "The given data was invalid.").with_details(details)
153 }
154
155 pub fn with_details(mut self, details: serde_json::Value) -> Self {
157 self.details = Some(details);
158 self
159 }
160
161 pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
163 self.source = Some(source.into());
164 self
165 }
166
167 pub fn with_kind(mut self, kind: ErrorKind) -> Self {
169 self.kind = kind;
170 self
171 }
172
173 pub fn kind(&self) -> ErrorKind {
175 self.kind
176 }
177
178 pub fn status(&self) -> u16 {
180 self.kind.status()
181 }
182
183 pub fn message(&self) -> &str {
185 &self.message
186 }
187
188 pub fn details(&self) -> Option<&serde_json::Value> {
190 self.details.as_ref()
191 }
192
193 pub fn source_error(&self) -> Option<&anyhow::Error> {
195 self.source.as_ref()
196 }
197}
198
199impl fmt::Display for Error {
200 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
201 write!(f, "{}", self.message)?;
202 if let Some(source) = &self.source {
203 write!(f, ": {source}")?;
204 }
205 Ok(())
206 }
207}
208
209impl std::error::Error for Error {
210 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
211 self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
212 }
213}
214
215impl From<anyhow::Error> for Error {
220 fn from(source: anyhow::Error) -> Self {
221 Self::internal(source.to_string()).with_source(source)
222 }
223}
224
225impl From<std::io::Error> for Error {
226 fn from(source: std::io::Error) -> Self {
227 let kind = match source.kind() {
228 std::io::ErrorKind::NotFound => ErrorKind::NotFound,
229 std::io::ErrorKind::PermissionDenied => ErrorKind::Unauthorized,
230 _ => ErrorKind::Internal,
231 };
232 Self::new(kind, source.to_string()).with_source(anyhow::Error::new(source))
233 }
234}
235
236impl From<serde_json::Error> for Error {
237 fn from(source: serde_json::Error) -> Self {
238 Self::bad_request(format!("malformed JSON: {source}"))
239 .with_source(anyhow::Error::new(source))
240 }
241}
242
243impl From<std::fmt::Error> for Error {
244 fn from(source: std::fmt::Error) -> Self {
245 Self::internal(source.to_string())
246 }
247}
248
249pub type Result<T, E = Error> = std::result::Result<T, E>;
251
252pub trait Context<T> {
257 fn context(self, context: impl fmt::Display) -> Result<T>;
259
260 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T>;
262}
263
264impl<T, E> Context<T> for Result<T, E>
265where
266 E: Into<Error>,
267{
268 fn context(self, context: impl fmt::Display) -> Result<T> {
269 self.map_err(|e| {
270 let e = e.into();
271 let kind = e.kind();
272 Error::new(kind, format!("{context}: {e}"))
273 })
274 }
275
276 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
277 self.map_err(|e| {
278 let e = e.into();
279 let kind = e.kind();
280 Error::new(kind, format!("{}: {e}", context()))
281 })
282 }
283}
284
285impl<T> Context<T> for Option<T> {
286 fn context(self, context: impl fmt::Display) -> Result<T> {
287 self.ok_or_else(|| Error::internal(context.to_string()))
288 }
289
290 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
291 self.ok_or_else(|| Error::internal(context().to_string()))
292 }
293}
294
295#[cfg(test)]
296mod tests {
297 use super::*;
298
299 #[test]
300 fn kinds_map_to_statuses() {
301 assert_eq!(ErrorKind::Validation.status(), 422);
302 assert_eq!(ErrorKind::Unauthenticated.status(), 401);
303 assert_eq!(ErrorKind::Status(418).status(), 418);
304 assert!(ErrorKind::Internal.is_server_error());
305 assert!(!ErrorKind::NotFound.is_server_error());
306 }
307
308 #[test]
309 fn context_preserves_the_kind() {
310 let err: Result<()> = Err(Error::not_found("no such post"));
311 let err = err.context("loading the post").unwrap_err();
312 assert_eq!(err.kind(), ErrorKind::NotFound);
313 assert_eq!(err.message(), "loading the post: no such post");
314 }
315
316 #[test]
317 fn validation_errors_carry_their_details() {
318 let err = Error::validation(serde_json::json!({ "email": ["required"] }));
319 assert_eq!(err.status(), 422);
320 assert!(err.details().is_some());
321 }
322
323 #[test]
324 fn anyhow_errors_arrive_as_internal() {
325 let err: Error = anyhow::anyhow!("connection reset").into();
326 assert_eq!(err.status(), 500);
327 assert!(err.source_error().is_some());
328 }
329}