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 {
151 Self::new(ErrorKind::Validation, "The given data was invalid.").with_details(details)
152 }
153
154 pub fn with_details(mut self, details: serde_json::Value) -> Self {
156 self.details = Some(details);
157 self
158 }
159
160 pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
162 self.source = Some(source.into());
163 self
164 }
165
166 pub fn with_kind(mut self, kind: ErrorKind) -> Self {
168 self.kind = kind;
169 self
170 }
171
172 pub fn kind(&self) -> ErrorKind {
174 self.kind
175 }
176
177 pub fn status(&self) -> u16 {
179 self.kind.status()
180 }
181
182 pub fn message(&self) -> &str {
184 &self.message
185 }
186
187 pub fn details(&self) -> Option<&serde_json::Value> {
189 self.details.as_ref()
190 }
191
192 pub fn source_error(&self) -> Option<&anyhow::Error> {
194 self.source.as_ref()
195 }
196}
197
198impl fmt::Display for Error {
199 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200 write!(f, "{}", self.message)?;
201 if let Some(source) = &self.source {
202 write!(f, ": {source}")?;
203 }
204 Ok(())
205 }
206}
207
208impl std::error::Error for Error {
209 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
210 self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
211 }
212}
213
214impl From<anyhow::Error> for Error {
219 fn from(source: anyhow::Error) -> Self {
220 Self::internal(source.to_string()).with_source(source)
221 }
222}
223
224impl From<std::io::Error> for Error {
225 fn from(source: std::io::Error) -> Self {
226 let kind = match source.kind() {
227 std::io::ErrorKind::NotFound => ErrorKind::NotFound,
228 std::io::ErrorKind::PermissionDenied => ErrorKind::Unauthorized,
229 _ => ErrorKind::Internal,
230 };
231 Self::new(kind, source.to_string()).with_source(anyhow::Error::new(source))
232 }
233}
234
235impl From<serde_json::Error> for Error {
236 fn from(source: serde_json::Error) -> Self {
237 Self::bad_request(format!("malformed JSON: {source}"))
238 .with_source(anyhow::Error::new(source))
239 }
240}
241
242impl From<std::fmt::Error> for Error {
243 fn from(source: std::fmt::Error) -> Self {
244 Self::internal(source.to_string())
245 }
246}
247
248pub type Result<T, E = Error> = std::result::Result<T, E>;
250
251pub trait Context<T> {
256 fn context(self, context: impl fmt::Display) -> Result<T>;
258
259 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T>;
261}
262
263impl<T, E> Context<T> for Result<T, E>
264where
265 E: Into<Error>,
266{
267 fn context(self, context: impl fmt::Display) -> Result<T> {
268 self.map_err(|e| {
269 let e = e.into();
270 let kind = e.kind();
271 Error::new(kind, format!("{context}: {e}"))
272 })
273 }
274
275 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
276 self.map_err(|e| {
277 let e = e.into();
278 let kind = e.kind();
279 Error::new(kind, format!("{}: {e}", context()))
280 })
281 }
282}
283
284impl<T> Context<T> for Option<T> {
285 fn context(self, context: impl fmt::Display) -> Result<T> {
286 self.ok_or_else(|| Error::internal(context.to_string()))
287 }
288
289 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
290 self.ok_or_else(|| Error::internal(context().to_string()))
291 }
292}
293
294#[cfg(test)]
295mod tests {
296 use super::*;
297
298 #[test]
299 fn kinds_map_to_statuses() {
300 assert_eq!(ErrorKind::Validation.status(), 422);
301 assert_eq!(ErrorKind::Unauthenticated.status(), 401);
302 assert_eq!(ErrorKind::Status(418).status(), 418);
303 assert!(ErrorKind::Internal.is_server_error());
304 assert!(!ErrorKind::NotFound.is_server_error());
305 }
306
307 #[test]
308 fn context_preserves_the_kind() {
309 let err: Result<()> = Err(Error::not_found("no such post"));
310 let err = err.context("loading the post").unwrap_err();
311 assert_eq!(err.kind(), ErrorKind::NotFound);
312 assert_eq!(err.message(), "loading the post: no such post");
313 }
314
315 #[test]
316 fn validation_errors_carry_their_details() {
317 let err = Error::validation(serde_json::json!({ "email": ["required"] }));
318 assert_eq!(err.status(), 422);
319 assert!(err.details().is_some());
320 }
321
322 #[test]
323 fn anyhow_errors_arrive_as_internal() {
324 let err: Error = anyhow::anyhow!("connection reset").into();
325 assert_eq!(err.status(), 500);
326 assert!(err.source_error().is_some());
327 }
328}