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 validation(details: serde_json::Value) -> Self {
126 Self::new(ErrorKind::Validation, "The given data was invalid.").with_details(details)
127 }
128
129 pub fn with_details(mut self, details: serde_json::Value) -> Self {
131 self.details = Some(details);
132 self
133 }
134
135 pub fn with_source(mut self, source: impl Into<anyhow::Error>) -> Self {
137 self.source = Some(source.into());
138 self
139 }
140
141 pub fn with_kind(mut self, kind: ErrorKind) -> Self {
143 self.kind = kind;
144 self
145 }
146
147 pub fn kind(&self) -> ErrorKind {
149 self.kind
150 }
151
152 pub fn status(&self) -> u16 {
154 self.kind.status()
155 }
156
157 pub fn message(&self) -> &str {
159 &self.message
160 }
161
162 pub fn details(&self) -> Option<&serde_json::Value> {
164 self.details.as_ref()
165 }
166
167 pub fn source_error(&self) -> Option<&anyhow::Error> {
169 self.source.as_ref()
170 }
171}
172
173impl fmt::Display for Error {
174 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
175 write!(f, "{}", self.message)?;
176 if let Some(source) = &self.source {
177 write!(f, ": {source}")?;
178 }
179 Ok(())
180 }
181}
182
183impl std::error::Error for Error {
184 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
185 self.source.as_ref().map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
186 }
187}
188
189impl From<anyhow::Error> for Error {
194 fn from(source: anyhow::Error) -> Self {
195 Self::internal(source.to_string()).with_source(source)
196 }
197}
198
199impl From<std::io::Error> for Error {
200 fn from(source: std::io::Error) -> Self {
201 let kind = match source.kind() {
202 std::io::ErrorKind::NotFound => ErrorKind::NotFound,
203 std::io::ErrorKind::PermissionDenied => ErrorKind::Unauthorized,
204 _ => ErrorKind::Internal,
205 };
206 Self::new(kind, source.to_string()).with_source(anyhow::Error::new(source))
207 }
208}
209
210impl From<serde_json::Error> for Error {
211 fn from(source: serde_json::Error) -> Self {
212 Self::bad_request(format!("malformed JSON: {source}"))
213 .with_source(anyhow::Error::new(source))
214 }
215}
216
217impl From<std::fmt::Error> for Error {
218 fn from(source: std::fmt::Error) -> Self {
219 Self::internal(source.to_string())
220 }
221}
222
223pub type Result<T, E = Error> = std::result::Result<T, E>;
225
226pub trait Context<T> {
231 fn context(self, context: impl fmt::Display) -> Result<T>;
233
234 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T>;
236}
237
238impl<T, E> Context<T> for Result<T, E>
239where
240 E: Into<Error>,
241{
242 fn context(self, context: impl fmt::Display) -> Result<T> {
243 self.map_err(|e| {
244 let e = e.into();
245 let kind = e.kind();
246 Error::new(kind, format!("{context}: {e}"))
247 })
248 }
249
250 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
251 self.map_err(|e| {
252 let e = e.into();
253 let kind = e.kind();
254 Error::new(kind, format!("{}: {e}", context()))
255 })
256 }
257}
258
259impl<T> Context<T> for Option<T> {
260 fn context(self, context: impl fmt::Display) -> Result<T> {
261 self.ok_or_else(|| Error::internal(context.to_string()))
262 }
263
264 fn with_context<C: fmt::Display>(self, context: impl FnOnce() -> C) -> Result<T> {
265 self.ok_or_else(|| Error::internal(context().to_string()))
266 }
267}
268
269#[cfg(test)]
270mod tests {
271 use super::*;
272
273 #[test]
274 fn kinds_map_to_statuses() {
275 assert_eq!(ErrorKind::Validation.status(), 422);
276 assert_eq!(ErrorKind::Unauthenticated.status(), 401);
277 assert_eq!(ErrorKind::Status(418).status(), 418);
278 assert!(ErrorKind::Internal.is_server_error());
279 assert!(!ErrorKind::NotFound.is_server_error());
280 }
281
282 #[test]
283 fn context_preserves_the_kind() {
284 let err: Result<()> = Err(Error::not_found("no such post"));
285 let err = err.context("loading the post").unwrap_err();
286 assert_eq!(err.kind(), ErrorKind::NotFound);
287 assert_eq!(err.message(), "loading the post: no such post");
288 }
289
290 #[test]
291 fn validation_errors_carry_their_details() {
292 let err = Error::validation(serde_json::json!({ "email": ["required"] }));
293 assert_eq!(err.status(), 422);
294 assert!(err.details().is_some());
295 }
296
297 #[test]
298 fn anyhow_errors_arrive_as_internal() {
299 let err: Error = anyhow::anyhow!("connection reset").into();
300 assert_eq!(err.status(), 500);
301 assert!(err.source_error().is_some());
302 }
303}