1#![cfg_attr(docsrs, feature(doc_auto_cfg, doc_cfg))]
2#![doc = include_str!("../README.md")]
3#![allow(renamed_and_removed_lints)] #![allow(unknown_lints)] #![warn(missing_docs)]
7#![warn(noop_method_call)]
8#![warn(unreachable_pub)]
9#![warn(clippy::all)]
10#![deny(clippy::await_holding_lock)]
11#![deny(clippy::cargo_common_metadata)]
12#![deny(clippy::cast_lossless)]
13#![deny(clippy::checked_conversions)]
14#![warn(clippy::cognitive_complexity)]
15#![deny(clippy::debug_assert_with_mut_call)]
16#![deny(clippy::exhaustive_enums)]
17#![deny(clippy::exhaustive_structs)]
18#![deny(clippy::expl_impl_clone_on_copy)]
19#![deny(clippy::fallible_impl_from)]
20#![deny(clippy::implicit_clone)]
21#![deny(clippy::large_stack_arrays)]
22#![warn(clippy::manual_ok_or)]
23#![deny(clippy::missing_docs_in_private_items)]
24#![warn(clippy::needless_borrow)]
25#![warn(clippy::needless_pass_by_value)]
26#![warn(clippy::option_option)]
27#![deny(clippy::print_stderr)]
28#![deny(clippy::print_stdout)]
29#![warn(clippy::rc_buffer)]
30#![deny(clippy::ref_option_ref)]
31#![warn(clippy::semicolon_if_nothing_returned)]
32#![warn(clippy::trait_duplication_in_bounds)]
33#![deny(clippy::unchecked_duration_subtraction)]
34#![deny(clippy::unnecessary_wraps)]
35#![warn(clippy::unseparated_literal_suffix)]
36#![deny(clippy::unwrap_used)]
37#![deny(clippy::mod_module_files)]
38#![allow(clippy::let_unit_value)] #![allow(clippy::uninlined_format_args)]
40#![allow(clippy::significant_drop_in_scrutinee)] #![allow(clippy::result_large_err)] #![allow(clippy::needless_raw_string_hashes)] #![allow(clippy::needless_lifetimes)] use std::error::Error;
47use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
48use std::iter;
49
50#[derive(Debug, Clone)]
59pub struct RetryError<E> {
60 doing: String,
62 errors: Vec<(Attempt, E)>,
64 n_errors: usize,
69}
70
71#[derive(Debug, Clone)]
73enum Attempt {
74 Single(usize),
76 Range(usize, usize),
78}
79
80impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
83
84impl<E> RetryError<E> {
85 pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
96 RetryError {
97 doing: doing.into(),
98 errors: Vec::new(),
99 n_errors: 0,
100 }
101 }
102 pub fn push<T>(&mut self, err: T)
107 where
108 T: Into<E>,
109 {
110 if self.n_errors < usize::MAX {
111 self.n_errors += 1;
112 let attempt = Attempt::Single(self.n_errors);
113 self.errors.push((attempt, err.into()));
114 }
115 }
116
117 pub fn sources(&self) -> impl Iterator<Item = &E> {
120 self.errors.iter().map(|(_, e)| e)
121 }
122
123 pub fn len(&self) -> usize {
125 self.errors.len()
126 }
127
128 pub fn is_empty(&self) -> bool {
130 self.errors.is_empty()
131 }
132
133 pub fn dedup_by<F>(&mut self, same_err: F)
138 where
139 F: Fn(&E, &E) -> bool,
140 {
141 let mut old_errs = Vec::new();
142 std::mem::swap(&mut old_errs, &mut self.errors);
143
144 for (attempt, err) in old_errs {
145 if let Some((ref mut last_attempt, last_err)) = self.errors.last_mut() {
146 if same_err(last_err, &err) {
147 last_attempt.grow();
148 } else {
149 self.errors.push((attempt, err));
150 }
151 } else {
152 self.errors.push((attempt, err));
153 }
154 }
155 }
156}
157
158impl<E: PartialEq<E>> RetryError<E> {
159 pub fn dedup(&mut self) {
162 self.dedup_by(PartialEq::eq);
163 }
164}
165
166impl Attempt {
167 fn grow(&mut self) {
169 *self = match *self {
170 Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
171 Attempt::Range(first, last) => Attempt::Range(first, last + 1),
172 };
173 }
174}
175
176impl<E, T> Extend<T> for RetryError<E>
177where
178 T: Into<E>,
179{
180 fn extend<C>(&mut self, iter: C)
181 where
182 C: IntoIterator<Item = T>,
183 {
184 for item in iter.into_iter() {
185 self.push(item);
186 }
187 }
188}
189
190impl<E> IntoIterator for RetryError<E> {
191 type Item = E;
192 type IntoIter = std::vec::IntoIter<E>;
193 #[allow(clippy::needless_collect)]
194 fn into_iter(self) -> Self::IntoIter {
199 let v: Vec<_> = self.errors.into_iter().map(|x| x.1).collect();
200 v.into_iter()
201 }
202}
203
204impl Display for Attempt {
205 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
206 match self {
207 Attempt::Single(idx) => write!(f, "Attempt {}", idx),
208 Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
209 }
210 }
211}
212
213impl<E: AsRef<dyn Error>> Display for RetryError<E> {
214 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
215 match self.n_errors {
216 0 => write!(f, "Unable to {}. (No errors given)", self.doing),
217 1 => {
218 write!(f, "Unable to {}: ", self.doing)?;
219 fmt_error_with_sources(self.errors[0].1.as_ref(), f)
220 }
221 n => {
222 write!(
223 f,
224 "Tried to {} {} times, but all attempts failed",
225 self.doing, n
226 )?;
227
228 for (attempt, e) in &self.errors {
229 write!(f, "\n{}: ", attempt)?;
230 fmt_error_with_sources(e.as_ref(), f)?;
231 }
232 Ok(())
233 }
234 }
235 }
236}
237
238pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
279 let mut last = String::new();
280 let mut sep = iter::once("").chain(iter::repeat(": "));
281 loop {
282 let this = e.to_string();
283 if !last.contains(&this) {
284 write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
285 }
286 last = this;
287
288 if let Some(ne) = e.source() {
289 e = ne;
290 } else {
291 break;
292 }
293 }
294 Ok(())
295}
296
297#[cfg(test)]
298mod test {
299 #![allow(clippy::bool_assert_comparison)]
301 #![allow(clippy::clone_on_copy)]
302 #![allow(clippy::dbg_macro)]
303 #![allow(clippy::mixed_attributes_style)]
304 #![allow(clippy::print_stderr)]
305 #![allow(clippy::print_stdout)]
306 #![allow(clippy::single_char_pattern)]
307 #![allow(clippy::unwrap_used)]
308 #![allow(clippy::unchecked_duration_subtraction)]
309 #![allow(clippy::useless_vec)]
310 #![allow(clippy::needless_pass_by_value)]
311 use super::*;
313 use derive_more::From;
314
315 #[test]
316 fn bad_parse1() {
317 let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
318 if let Err(e) = "maybe".parse::<bool>() {
319 err.push(e);
320 }
321 if let Err(e) = "a few".parse::<u32>() {
322 err.push(e);
323 }
324 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
325 err.push(e);
326 }
327 let disp = format!("{}", err);
328 assert_eq!(
329 disp,
330 "\
331Tried to convert some things 3 times, but all attempts failed
332Attempt 1: provided string was not `true` or `false`
333Attempt 2: invalid digit found in string
334Attempt 3: invalid IP address syntax"
335 );
336 }
337
338 #[test]
339 fn no_problems() {
340 let empty: RetryError<anyhow::Error> =
341 RetryError::in_attempt_to("immanentize the eschaton");
342 let disp = format!("{}", empty);
343 assert_eq!(
344 disp,
345 "Unable to immanentize the eschaton. (No errors given)"
346 );
347 }
348
349 #[test]
350 fn one_problem() {
351 let mut err: RetryError<anyhow::Error> =
352 RetryError::in_attempt_to("connect to torproject.org");
353 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
354 err.push(e);
355 }
356 let disp = format!("{}", err);
357 assert_eq!(
358 disp,
359 "Unable to connect to torproject.org: invalid IP address syntax"
360 );
361 }
362
363 #[test]
364 fn operations() {
365 use std::num::ParseIntError;
366
367 #[derive(From, Clone, Debug, Eq, PartialEq)]
368 struct Wrapper(ParseIntError);
369
370 impl AsRef<dyn Error + 'static> for Wrapper {
371 fn as_ref(&self) -> &(dyn Error + 'static) {
372 &self.0
373 }
374 }
375
376 let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
377 assert!(err.is_empty());
378 assert_eq!(err.len(), 0);
379 err.extend(
380 vec!["not", "your", "number"]
381 .iter()
382 .filter_map(|s| s.parse::<u16>().err())
383 .map(Wrapper),
384 );
385 assert!(!err.is_empty());
386 assert_eq!(err.len(), 3);
387
388 let cloned = err.clone();
389 for (s1, s2) in err.sources().zip(cloned.sources()) {
390 assert_eq!(s1, s2);
391 }
392
393 err.dedup();
394 let disp = format!("{}", err);
395 assert_eq!(
396 disp,
397 "\
398Tried to parse some integers 3 times, but all attempts failed
399Attempts 1..3: invalid digit found in string"
400 );
401 }
402
403 #[test]
404 fn overflow() {
405 use std::num::ParseIntError;
406 let mut err: RetryError<ParseIntError> =
407 RetryError::in_attempt_to("parse too many integers");
408 assert!(err.is_empty());
409 let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
410 .iter()
411 .filter_map(|s| s.parse::<u16>().err())
412 .collect();
413 err.n_errors = usize::MAX;
414 err.errors.push((
415 Attempt::Range(1, err.n_errors),
416 errors.pop().expect("parser did not fail"),
417 ));
418 assert!(err.n_errors == usize::MAX);
419 assert!(err.len() == 1);
420
421 err.push(errors.pop().expect("parser did not fail"));
422 assert!(err.n_errors == usize::MAX);
423 assert!(err.len() == 1);
424 }
425}