1#![cfg_attr(docsrs, feature(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_time_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)] #![allow(mismatched_lifetime_syntaxes)] use std::error::Error;
48use std::fmt::{self, Debug, Display, Error as FmtError, Formatter};
49use std::iter;
50
51#[derive(Debug, Clone)]
60pub struct RetryError<E> {
61 doing: String,
63 errors: Vec<(Attempt, E)>,
65 n_errors: usize,
70}
71
72#[derive(Debug, Clone)]
74enum Attempt {
75 Single(usize),
77 Range(usize, usize),
79}
80
81impl<E: Debug + AsRef<dyn Error>> Error for RetryError<E> {}
84
85impl<E> RetryError<E> {
86 pub fn in_attempt_to<T: Into<String>>(doing: T) -> Self {
97 RetryError {
98 doing: doing.into(),
99 errors: Vec::new(),
100 n_errors: 0,
101 }
102 }
103 pub fn push<T>(&mut self, err: T)
108 where
109 T: Into<E>,
110 {
111 if self.n_errors < usize::MAX {
112 self.n_errors += 1;
113 let attempt = Attempt::Single(self.n_errors);
114 self.errors.push((attempt, err.into()));
115 }
116 }
117
118 pub fn sources(&self) -> impl Iterator<Item = &E> {
121 self.errors.iter().map(|(_, e)| e)
122 }
123
124 pub fn len(&self) -> usize {
126 self.errors.len()
127 }
128
129 pub fn is_empty(&self) -> bool {
131 self.errors.is_empty()
132 }
133
134 pub fn dedup_by<F>(&mut self, same_err: F)
139 where
140 F: Fn(&E, &E) -> bool,
141 {
142 let mut old_errs = Vec::new();
143 std::mem::swap(&mut old_errs, &mut self.errors);
144
145 for (attempt, err) in old_errs {
146 if let Some((last_attempt, last_err)) = self.errors.last_mut() {
147 if same_err(last_err, &err) {
148 last_attempt.grow();
149 } else {
150 self.errors.push((attempt, err));
151 }
152 } else {
153 self.errors.push((attempt, err));
154 }
155 }
156 }
157}
158
159impl<E: PartialEq<E>> RetryError<E> {
160 pub fn dedup(&mut self) {
163 self.dedup_by(PartialEq::eq);
164 }
165}
166
167impl Attempt {
168 fn grow(&mut self) {
170 *self = match *self {
171 Attempt::Single(idx) => Attempt::Range(idx, idx + 1),
172 Attempt::Range(first, last) => Attempt::Range(first, last + 1),
173 };
174 }
175}
176
177impl<E, T> Extend<T> for RetryError<E>
178where
179 T: Into<E>,
180{
181 fn extend<C>(&mut self, iter: C)
182 where
183 C: IntoIterator<Item = T>,
184 {
185 for item in iter.into_iter() {
186 self.push(item);
187 }
188 }
189}
190
191impl<E> IntoIterator for RetryError<E> {
192 type Item = E;
193 type IntoIter = std::vec::IntoIter<E>;
194 #[allow(clippy::needless_collect)]
195 fn into_iter(self) -> Self::IntoIter {
200 let v: Vec<_> = self.errors.into_iter().map(|x| x.1).collect();
201 v.into_iter()
202 }
203}
204
205impl Display for Attempt {
206 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
207 match self {
208 Attempt::Single(idx) => write!(f, "Attempt {}", idx),
209 Attempt::Range(first, last) => write!(f, "Attempts {}..{}", first, last),
210 }
211 }
212}
213
214impl<E: AsRef<dyn Error>> Display for RetryError<E> {
215 fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), FmtError> {
216 match self.n_errors {
217 0 => write!(f, "Unable to {}. (No errors given)", self.doing),
218 1 => {
219 write!(f, "Unable to {}: ", self.doing)?;
220 fmt_error_with_sources(self.errors[0].1.as_ref(), f)
221 }
222 n => {
223 write!(
224 f,
225 "Tried to {} {} times, but all attempts failed",
226 self.doing, n
227 )?;
228
229 for (attempt, e) in &self.errors {
230 write!(f, "\n{}: ", attempt)?;
231 fmt_error_with_sources(e.as_ref(), f)?;
232 }
233 Ok(())
234 }
235 }
236 }
237}
238
239pub fn fmt_error_with_sources(mut e: &dyn Error, f: &mut fmt::Formatter) -> fmt::Result {
280 let mut last = String::new();
285 let mut sep = iter::once("").chain(iter::repeat(": "));
286 loop {
287 let this = e.to_string();
288 if !last.contains(&this) {
289 write!(f, "{}{}", sep.next().expect("repeat ended"), &this)?;
290 }
291 last = this;
292
293 if let Some(ne) = e.source() {
294 e = ne;
295 } else {
296 break;
297 }
298 }
299 Ok(())
300}
301
302#[cfg(test)]
303mod test {
304 #![allow(clippy::bool_assert_comparison)]
306 #![allow(clippy::clone_on_copy)]
307 #![allow(clippy::dbg_macro)]
308 #![allow(clippy::mixed_attributes_style)]
309 #![allow(clippy::print_stderr)]
310 #![allow(clippy::print_stdout)]
311 #![allow(clippy::single_char_pattern)]
312 #![allow(clippy::unwrap_used)]
313 #![allow(clippy::unchecked_time_subtraction)]
314 #![allow(clippy::useless_vec)]
315 #![allow(clippy::needless_pass_by_value)]
316 use super::*;
318 use derive_more::From;
319
320 #[test]
321 fn bad_parse1() {
322 let mut err: RetryError<anyhow::Error> = RetryError::in_attempt_to("convert some things");
323 if let Err(e) = "maybe".parse::<bool>() {
324 err.push(e);
325 }
326 if let Err(e) = "a few".parse::<u32>() {
327 err.push(e);
328 }
329 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
330 err.push(e);
331 }
332 let disp = format!("{}", err);
333 assert_eq!(
334 disp,
335 "\
336Tried to convert some things 3 times, but all attempts failed
337Attempt 1: provided string was not `true` or `false`
338Attempt 2: invalid digit found in string
339Attempt 3: invalid IP address syntax"
340 );
341 }
342
343 #[test]
344 fn no_problems() {
345 let empty: RetryError<anyhow::Error> =
346 RetryError::in_attempt_to("immanentize the eschaton");
347 let disp = format!("{}", empty);
348 assert_eq!(
349 disp,
350 "Unable to immanentize the eschaton. (No errors given)"
351 );
352 }
353
354 #[test]
355 fn one_problem() {
356 let mut err: RetryError<anyhow::Error> =
357 RetryError::in_attempt_to("connect to torproject.org");
358 if let Err(e) = "the_g1b50n".parse::<std::net::IpAddr>() {
359 err.push(e);
360 }
361 let disp = format!("{}", err);
362 assert_eq!(
363 disp,
364 "Unable to connect to torproject.org: invalid IP address syntax"
365 );
366 }
367
368 #[test]
369 fn operations() {
370 use std::num::ParseIntError;
371
372 #[derive(From, Clone, Debug, Eq, PartialEq)]
373 struct Wrapper(ParseIntError);
374
375 impl AsRef<dyn Error + 'static> for Wrapper {
376 fn as_ref(&self) -> &(dyn Error + 'static) {
377 &self.0
378 }
379 }
380
381 let mut err: RetryError<Wrapper> = RetryError::in_attempt_to("parse some integers");
382 assert!(err.is_empty());
383 assert_eq!(err.len(), 0);
384 err.extend(
385 vec!["not", "your", "number"]
386 .iter()
387 .filter_map(|s| s.parse::<u16>().err())
388 .map(Wrapper),
389 );
390 assert!(!err.is_empty());
391 assert_eq!(err.len(), 3);
392
393 let cloned = err.clone();
394 for (s1, s2) in err.sources().zip(cloned.sources()) {
395 assert_eq!(s1, s2);
396 }
397
398 err.dedup();
399 let disp = format!("{}", err);
400 assert_eq!(
401 disp,
402 "\
403Tried to parse some integers 3 times, but all attempts failed
404Attempts 1..3: invalid digit found in string"
405 );
406 }
407
408 #[test]
409 fn overflow() {
410 use std::num::ParseIntError;
411 let mut err: RetryError<ParseIntError> =
412 RetryError::in_attempt_to("parse too many integers");
413 assert!(err.is_empty());
414 let mut errors: Vec<ParseIntError> = vec!["no", "numbers"]
415 .iter()
416 .filter_map(|s| s.parse::<u16>().err())
417 .collect();
418 err.n_errors = usize::MAX;
419 err.errors.push((
420 Attempt::Range(1, err.n_errors),
421 errors.pop().expect("parser did not fail"),
422 ));
423 assert!(err.n_errors == usize::MAX);
424 assert!(err.len() == 1);
425
426 err.push(errors.pop().expect("parser did not fail"));
427 assert!(err.n_errors == usize::MAX);
428 assert!(err.len() == 1);
429 }
430}