1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Recipes and patterns for flowing recovery information.
//!
//! A cookbook of practical examples for implementing [`Recovery`] effectively.
//! All examples use [`ohno`](https://docs.rs/ohno) for error definitions.
//!
//! # Flow from inner errors
//!
//! When your error wraps an inner error that already implements [`Recovery`],
//! flow its recovery information through the `#[from]` attribute. This
//! preserves the classification made by the layer closest to the root cause.
//!
//! ```rust
//! use recoverable::{Recovery, RecoveryInfo, RecoveryKind};
//!
//! #[ohno::error]
//! struct DatabaseError {
//! recovery: RecoveryInfo,
//! }
//!
//! impl Recovery for DatabaseError {
//! fn recovery(&self) -> RecoveryInfo {
//! self.recovery.clone()
//! }
//! }
//!
//! /// The `#[from]` attribute flows recovery info from `DatabaseError`
//! /// automatically — `error.recovery()` is called during conversion.
//! #[ohno::error]
//! #[from(DatabaseError(recovery: error.recovery()))]
//! struct ServiceError {
//! recovery: RecoveryInfo,
//! }
//!
//! impl Recovery for ServiceError {
//! fn recovery(&self) -> RecoveryInfo {
//! self.recovery.clone()
//! }
//! }
//!
//! fn database_operation() -> Result<(), DatabaseError> {
//! Err(DatabaseError::caused_by(
//! RecoveryInfo::retry(),
//! "connection timed out",
//! ))
//! }
//!
//! fn service_operation() -> Result<(), ServiceError> {
//! // The ? operator converts DatabaseError into ServiceError,
//! // flowing recovery info automatically via #[from].
//! database_operation()?;
//! Ok(())
//! }
//!
//! let err = service_operation().unwrap_err();
//! assert_eq!(err.recovery().kind(), RecoveryKind::Retry);
//! ```
//!
//! Only override the inner classification when the outer context changes the
//! recoverability. For example, a transient inner error might become
//! non-recoverable if a retry budget is exhausted at the outer layer.
//!
//! # Non-recoverable errors
//!
//! When an error has no inner cause and the condition is permanent, use
//! [`RecoveryInfo::never()`] directly.
//!
//! If **all** states of an error are permanently non-recoverable and this is
//! unlikely to change, you do not need to implement [`Recovery`] at all.
//! Only implement the trait when at least some states may be recoverable or
//! when the recoverability classification may evolve in the future.
//!
//! ```rust
//! use recoverable::{Recovery, RecoveryInfo, RecoveryKind};
//!
//! #[ohno::error]
//! struct ConfigError {
//! recovery: RecoveryInfo,
//! }
//!
//! impl Recovery for ConfigError {
//! fn recovery(&self) -> RecoveryInfo {
//! self.recovery.clone()
//! }
//! }
//!
//! let err = ConfigError::caused_by(
//! RecoveryInfo::never(),
//! "missing required field: database_url",
//! );
//! assert_eq!(err.recovery().kind(), RecoveryKind::Never);
//! ```
//!
//! # Heuristic recovery
//!
//! Sometimes an inner error does not implement [`Recovery`] but the operation
//! is potentially recoverable. In these cases, use heuristics to derive
//! recovery information. A common pattern is detecting [`std::io::Error`] as
//! the root cause and converting its [`ErrorKind`](std::io::ErrorKind) into
//! [`RecoveryInfo`] via the built-in [`RecoveryInfo::from`]
//! conversion.
//!
//! The `#[from]` attribute lets you apply the heuristic at conversion time:
//!
//! ```rust
//! use std::io;
//!
//! use recoverable::{Recovery, RecoveryInfo, RecoveryKind};
//!
//! /// `std::io::Error` does not implement `Recovery`, so we derive
//! /// recoverability from its `ErrorKind` using the built-in conversion.
//! #[ohno::error]
//! #[from(io::Error(recovery: error.kind().into()))]
//! struct StorageError {
//! recovery: RecoveryInfo,
//! }
//!
//! impl Recovery for StorageError {
//! fn recovery(&self) -> RecoveryInfo {
//! self.recovery.clone()
//! }
//! }
//!
//! // A connection-reset IO error is classified as transient.
//! let err = StorageError::from(io::Error::from(io::ErrorKind::ConnectionReset));
//! assert_eq!(err.recovery().kind(), RecoveryKind::Retry);
//!
//! // A "not found" IO error is classified as permanent.
//! let err = StorageError::from(io::Error::from(io::ErrorKind::NotFound));
//! assert_eq!(err.recovery().kind(), RecoveryKind::Never);
//! ```
//!
//! When the IO error is buried deeper in the cause chain and you need to
//! walk it manually, use [`Error::source`](std::error::Error::source):
//!
//! ```rust
//! use std::error::Error;
//! use std::io;
//!
//! use recoverable::{Recovery, RecoveryInfo, RecoveryKind};
//!
//! #[ohno::error]
//! struct ProtocolError {
//! recovery: RecoveryInfo,
//! }
//!
//! impl ProtocolError {
//! fn from_source(source: impl Into<Box<dyn Error + Send + Sync>>) -> Self {
//! let source: Box<dyn Error + Send + Sync> = source.into();
//! let recovery = io_recovery_from_chain(source.as_ref());
//! Self::caused_by(recovery, source)
//! }
//! }
//!
//! impl Recovery for ProtocolError {
//! fn recovery(&self) -> RecoveryInfo {
//! self.recovery.clone()
//! }
//! }
//!
//! /// Walk the error chain looking for a `std::io::Error` and derive
//! /// recovery info from its `ErrorKind`.
//! fn io_recovery_from_chain(err: &(dyn Error + 'static)) -> RecoveryInfo {
//! std::iter::successors(Some(err), |e| (*e).source())
//! .find_map(|e| e.downcast_ref::<io::Error>())
//! .map(|io_err| RecoveryInfo::from(io_err.kind()))
//! // No IO error found — assume non-recoverable.
//! .unwrap_or_else(RecoveryInfo::never)
//! }
//! ```
//!
//! The `From<ErrorKind> for RecoveryInfo` conversion provides opinionated
//! defaults for common IO error kinds. See its documentation for the full
//! classification table. If the defaults don't match your use case,
//! implement your own mapping.
use crate::*;