1use std::error::Error;
5use std::fmt;
6use std::sync::Arc;
7use std::time::Duration;
8
9#[derive(Debug, Clone, PartialEq, Eq, Hash)]
18#[non_exhaustive]
19pub enum Class {
20 Network,
23 Unavailable {
25 code: Option<Arc<str>>,
27 },
28 RateLimited {
30 retry_after: Option<Duration>,
32 },
33 Unauthorized,
35 InvalidRequest,
39 VenueRefusal {
42 code: Option<Arc<str>>,
44 },
45 Restricted,
48 Decode,
50}
51
52impl Class {
53 pub fn is_retriable(&self) -> bool {
61 matches!(
62 self,
63 Self::Network | Self::Unavailable { .. } | Self::RateLimited { .. }
64 )
65 }
66
67 pub fn retry_after(&self) -> Option<Duration> {
70 match self {
71 Self::RateLimited { retry_after } => retry_after.filter(|wait| !wait.is_zero()),
72 _ => None,
73 }
74 }
75
76 pub fn code(&self) -> Option<&str> {
79 match self {
80 Self::Unavailable { code } | Self::VenueRefusal { code } => code.as_deref(),
81 _ => None,
82 }
83 }
84
85 pub fn with_code(self, code: impl Into<Arc<str>>) -> Self {
89 match self {
90 Self::Unavailable { .. } => Self::Unavailable {
91 code: Some(code.into()),
92 },
93 Self::VenueRefusal { .. } => Self::VenueRefusal {
94 code: Some(code.into()),
95 },
96 other => other,
97 }
98 }
99
100 pub fn with_retry_after(self, retry_after: Option<Duration>) -> Self {
104 match self {
105 Self::RateLimited { .. } => Self::RateLimited {
106 retry_after: retry_after.filter(|wait| !wait.is_zero()),
107 },
108 other => other,
109 }
110 }
111}
112
113pub trait Classify: Error {
118 fn class(&self) -> Class;
120
121 fn is_fault(&self) -> bool {
126 true
127 }
128
129 fn retry_after(&self) -> Option<Duration> {
133 self.class().retry_after()
134 }
135
136 fn is_retriable(&self) -> bool {
139 self.class().is_retriable()
140 }
141}
142
143#[derive(Debug)]
156pub struct ClassifiedError {
157 pub class: Class,
159 pub source: Box<dyn Error + Send + Sync + 'static>,
161 fault: bool,
162 retry_after: Option<Duration>,
163}
164
165impl ClassifiedError {
166 pub fn new(class: Class, source: impl Into<Box<dyn Error + Send + Sync + 'static>>) -> Self {
169 let retry_after = class.retry_after();
170 Self {
171 class,
172 source: source.into(),
173 fault: true,
174 retry_after,
175 }
176 }
177
178 pub fn is_fault(&self) -> bool {
180 self.fault
181 }
182
183 pub fn retry_after(&self) -> Option<Duration> {
185 self.retry_after
186 }
187
188 pub fn is_retriable(&self) -> bool {
190 self.class.is_retriable()
191 }
192}
193
194impl fmt::Display for ClassifiedError {
195 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
196 fmt::Display::fmt(&self.source, f)
197 }
198}
199
200impl Error for ClassifiedError {
201 fn source(&self) -> Option<&(dyn Error + 'static)> {
202 self.source.source()
203 }
204}
205
206impl<E> From<E> for ClassifiedError
207where
208 E: Classify + Send + Sync + 'static,
209{
210 fn from(err: E) -> Self {
211 Self {
212 class: err.class(),
213 fault: err.is_fault(),
214 retry_after: err.retry_after().filter(|wait| !wait.is_zero()),
216 source: Box::new(err),
217 }
218 }
219}
220
221#[cfg(test)]
222mod tests {
223 use super::*;
224
225 #[derive(Debug)]
228 struct Banned;
229
230 impl fmt::Display for Banned {
231 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
232 f.write_str("banned")
233 }
234 }
235
236 impl Error for Banned {}
237
238 impl Classify for Banned {
239 fn class(&self) -> Class {
240 Class::Restricted
241 }
242 fn is_fault(&self) -> bool {
243 false
244 }
245 fn retry_after(&self) -> Option<Duration> {
246 Some(Duration::from_secs(120))
247 }
248 }
249
250 #[derive(Debug)]
251 struct Throttled;
252
253 impl fmt::Display for Throttled {
254 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
255 f.write_str("throttled")
256 }
257 }
258
259 impl Error for Throttled {}
260
261 impl Classify for Throttled {
262 fn class(&self) -> Class {
263 Class::RateLimited {
264 retry_after: Some(Duration::from_secs(3)),
265 }
266 }
267 }
268
269 #[test]
270 fn only_network_unavailable_and_rate_limited_are_retriable() {
271 let classes = [
272 (Class::Network, true),
273 (Class::Unavailable { code: None }, true),
274 (Class::RateLimited { retry_after: None }, true),
275 (Class::Unauthorized, false),
276 (Class::InvalidRequest, false),
277 (Class::VenueRefusal { code: None }, false),
278 (Class::Restricted, false),
279 (Class::Decode, false),
280 ];
281 for (class, retriable) in classes {
282 assert_eq!(class.is_retriable(), retriable, "{class:?}");
283 }
284 }
285
286 #[test]
287 fn a_code_and_a_wait_land_only_where_a_class_has_room_for_them() {
288 assert_eq!(
289 Class::VenueRefusal { code: None }.with_code("-1121").code(),
290 Some("-1121")
291 );
292 assert_eq!(
293 Class::Unavailable { code: None }
294 .with_code("internal")
295 .code(),
296 Some("internal")
297 );
298 assert_eq!(
299 Class::Unauthorized.with_code("x"),
300 Class::Unauthorized,
301 "a class without a code field is unchanged"
302 );
303
304 let wait = Some(Duration::from_secs(2));
305 assert_eq!(
306 Class::RateLimited { retry_after: None }
307 .with_retry_after(wait)
308 .retry_after(),
309 wait
310 );
311 assert_eq!(
312 Class::RateLimited { retry_after: None }
313 .with_retry_after(Some(Duration::ZERO))
314 .retry_after(),
315 None,
316 "a zero wait asks for nothing"
317 );
318 assert_eq!(
319 Class::RateLimited {
320 retry_after: Some(Duration::ZERO)
321 }
322 .retry_after(),
323 None,
324 "nor when the class was built with one"
325 );
326 assert_eq!(Class::Restricted.with_retry_after(wait), Class::Restricted);
327 }
328
329 #[test]
330 fn the_defaults_read_the_class() {
331 assert!(Throttled.is_fault());
332 assert_eq!(
333 Classify::retry_after(&Throttled),
334 Some(Duration::from_secs(3))
335 );
336 assert!(Classify::is_retriable(&Throttled));
337 }
338
339 #[test]
340 fn a_classified_error_keeps_what_the_error_reported() {
341 let err = ClassifiedError::from(Banned);
342 assert_eq!(err.class, Class::Restricted);
343 assert!(!err.is_fault());
344 assert_eq!(err.retry_after(), Some(Duration::from_secs(120)));
345 assert!(!err.is_retriable());
346 assert!(err.source.downcast_ref::<Banned>().is_some());
347 }
348
349 #[test]
350 fn a_classified_error_displays_as_its_source_and_continues_its_chain() {
351 let err: ClassifiedError = Throttled.into();
352 assert_eq!(err.to_string(), "throttled");
353 assert!(
354 Error::source(&err).is_none(),
355 "Throttled has no source of its own"
356 );
357 }
358
359 #[derive(Debug)]
361 struct ZeroWait;
362
363 impl fmt::Display for ZeroWait {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 f.write_str("zero wait")
366 }
367 }
368
369 impl Error for ZeroWait {}
370
371 impl Classify for ZeroWait {
372 fn class(&self) -> Class {
373 Class::Unavailable { code: None }
374 }
375 fn retry_after(&self) -> Option<Duration> {
376 Some(Duration::ZERO)
377 }
378 }
379
380 #[test]
381 fn a_classified_error_never_keeps_a_zero_wait() {
382 assert_eq!(ClassifiedError::from(ZeroWait).retry_after(), None);
383 }
384
385 #[test]
386 fn an_unclassified_error_takes_the_class_it_is_given() {
387 let err = ClassifiedError::new(
388 Class::RateLimited {
389 retry_after: Some(Duration::from_secs(1)),
390 },
391 "slow down",
392 );
393 assert!(err.is_fault());
394 assert_eq!(err.retry_after(), Some(Duration::from_secs(1)));
395 assert_eq!(err.to_string(), "slow down");
396 }
397
398 #[test]
399 fn the_question_mark_operator_classifies() {
400 fn fails() -> Result<(), ClassifiedError> {
401 let banned: Result<(), Banned> = Err(Banned);
402 banned?;
403 Ok(())
404 }
405 assert_eq!(fails().unwrap_err().class, Class::Restricted);
406 }
407}