1use anyhow::Result;
7use std::time::Duration;
8use tokio::time::sleep;
9
10#[derive(Debug, Clone)]
12pub struct RetryPolicy {
13 pub max_attempts: u32,
15
16 pub initial_delay: Duration,
18
19 pub backoff_multiplier: f64,
21
22 pub max_delay: Duration,
24}
25
26impl Default for RetryPolicy {
27 fn default() -> Self {
28 Self {
29 max_attempts: 3,
30 initial_delay: Duration::from_millis(100),
31 backoff_multiplier: 2.0,
32 max_delay: Duration::from_secs(5),
33 }
34 }
35}
36
37impl RetryPolicy {
38 pub fn no_retry() -> Self {
40 Self {
41 max_attempts: 1,
42 ..Default::default()
43 }
44 }
45
46 pub fn aggressive() -> Self {
48 Self {
49 max_attempts: 5,
50 initial_delay: Duration::from_millis(50),
51 backoff_multiplier: 1.5,
52 max_delay: Duration::from_secs(3),
53 }
54 }
55}
56
57pub async fn with_retry<F, T, E>(policy: &RetryPolicy, mut operation: F) -> Result<T>
59where
60 F: FnMut() -> Result<T, E>,
61 E: std::fmt::Display,
62{
63 let mut attempts = 0;
64 let mut delay = policy.initial_delay;
65
66 loop {
67 attempts += 1;
68
69 match operation() {
70 Ok(result) => return Ok(result),
71 Err(e) => {
72 if attempts >= policy.max_attempts {
73 return Err(anyhow::anyhow!(
74 "Operation failed after {} attempts: {}",
75 attempts,
76 e
77 ));
78 }
79
80 eprintln!(
81 "ā ļø Attempt {}/{} failed: {}. Retrying in {:?}...",
82 attempts, policy.max_attempts, e, delay
83 );
84
85 sleep(delay).await;
86
87 delay = Duration::from_secs_f64(
89 (delay.as_secs_f64() * policy.backoff_multiplier).min(policy.max_delay.as_secs_f64())
90 );
91 }
92 }
93 }
94}
95
96#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum ErrorCategory {
99 Network,
101
102 FileSystem,
104
105 Configuration,
107
108 Validation,
110
111 Dependency,
113
114 Timeout,
116
117 Unknown,
119}
120
121impl ErrorCategory {
122 pub fn is_retryable(&self) -> bool {
124 matches!(
125 self,
126 ErrorCategory::Network | ErrorCategory::FileSystem | ErrorCategory::Timeout
127 )
128 }
129}
130
131pub fn categorize_error(error: &anyhow::Error) -> ErrorCategory {
133 let error_str = error.to_string().to_lowercase();
134
135 if error_str.contains("network")
136 || error_str.contains("connection")
137 || error_str.contains("timeout")
138 || error_str.contains("dns")
139 {
140 ErrorCategory::Network
141 } else if error_str.contains("file")
142 || error_str.contains("directory")
143 || error_str.contains("permission")
144 || error_str.contains("io")
145 {
146 ErrorCategory::FileSystem
147 } else if error_str.contains("config") || error_str.contains("invalid") {
148 ErrorCategory::Configuration
149 } else if error_str.contains("dependency") || error_str.contains("version") {
150 ErrorCategory::Dependency
151 } else if error_str.contains("timeout") {
152 ErrorCategory::Timeout
153 } else {
154 ErrorCategory::Unknown
155 }
156}
157
158#[derive(Debug)]
160pub struct EnhancedError {
161 pub error: anyhow::Error,
162 pub category: ErrorCategory,
163 pub context: Vec<String>,
164 pub suggestions: Vec<String>,
165}
166
167impl EnhancedError {
168 pub fn new(error: anyhow::Error) -> Self {
170 let category = categorize_error(&error);
171 let (context, suggestions) = generate_context_and_suggestions(&category, &error);
172
173 Self {
174 error,
175 category,
176 context,
177 suggestions,
178 }
179 }
180
181 pub fn display(&self) -> String {
183 let mut output = format!("ā Error: {}\n", self.error);
184
185 if !self.context.is_empty() {
186 output.push_str("\nš Context:\n");
187 for ctx in &self.context {
188 output.push_str(&format!(" ⢠{}\n", ctx));
189 }
190 }
191
192 if !self.suggestions.is_empty() {
193 output.push_str("\nš” Suggestions:\n");
194 for suggestion in &self.suggestions {
195 output.push_str(&format!(" ⢠{}\n", suggestion));
196 }
197 }
198
199 output
200 }
201}
202
203fn generate_context_and_suggestions(
205 category: &ErrorCategory,
206 error: &anyhow::Error,
207) -> (Vec<String>, Vec<String>) {
208 let mut context = Vec::new();
209 let mut suggestions = Vec::new();
210
211 match category {
212 ErrorCategory::Network => {
213 context.push("Network operation failed".to_string());
214 suggestions.push("Check your internet connection".to_string());
215 suggestions.push("Verify firewall settings".to_string());
216 suggestions.push("Try again in a few moments".to_string());
217 }
218 ErrorCategory::FileSystem => {
219 context.push("File system operation failed".to_string());
220 suggestions.push("Check file permissions".to_string());
221 suggestions.push("Verify the path exists".to_string());
222 suggestions.push("Ensure sufficient disk space".to_string());
223 }
224 ErrorCategory::Configuration => {
225 context.push("Configuration error detected".to_string());
226 suggestions.push("Review your configuration file".to_string());
227 suggestions.push("Check environment variables".to_string());
228 suggestions.push("Refer to documentation for valid options".to_string());
229 }
230 ErrorCategory::Dependency => {
231 context.push("Dependency resolution failed".to_string());
232 suggestions.push("Check tool dependencies".to_string());
233 suggestions.push("Verify version compatibility".to_string());
234 suggestions.push("Run 'forge update' to sync dependencies".to_string());
235 }
236 ErrorCategory::Timeout => {
237 context.push("Operation timed out".to_string());
238 suggestions.push("The operation may need more time".to_string());
239 suggestions.push("Try increasing timeout settings".to_string());
240 suggestions.push("Check system resources".to_string());
241 }
242 ErrorCategory::Validation => {
243 context.push("Validation error".to_string());
244 suggestions.push("Review input data".to_string());
245 suggestions.push("Check for required fields".to_string());
246 }
247 ErrorCategory::Unknown => {
248 context.push(format!("Unexpected error: {}", error));
249 suggestions.push("Check logs for more details".to_string());
250 suggestions.push("Report this issue if it persists".to_string());
251 }
252 }
253
254 (context, suggestions)
255}
256
257pub type EnhancedResult<T> = Result<T, EnhancedError>;
259
260pub trait ToEnhanced<T> {
262 fn enhance(self) -> EnhancedResult<T>;
263}
264
265impl<T, E: Into<anyhow::Error>> ToEnhanced<T> for Result<T, E> {
266 fn enhance(self) -> EnhancedResult<T> {
267 self.map_err(|e| EnhancedError::new(e.into()))
268 }
269}
270
271#[cfg(test)]
272mod tests {
273 use super::*;
274
275 #[test]
276 fn test_error_categorization() {
277 let net_err = anyhow::anyhow!("Network connection failed");
278 assert_eq!(categorize_error(&net_err), ErrorCategory::Network);
279
280 let fs_err = anyhow::anyhow!("File not found");
281 assert_eq!(categorize_error(&fs_err), ErrorCategory::FileSystem);
282
283 let config_err = anyhow::anyhow!("Invalid config value");
284 assert_eq!(categorize_error(&config_err), ErrorCategory::Configuration);
285 }
286
287 #[test]
288 fn test_retryable() {
289 assert!(ErrorCategory::Network.is_retryable());
290 assert!(!ErrorCategory::Configuration.is_retryable());
291 }
292
293 #[test]
294 fn test_retry_policy() {
295 let policy = RetryPolicy::default();
296 assert_eq!(policy.max_attempts, 3);
297
298 let no_retry = RetryPolicy::no_retry();
299 assert_eq!(no_retry.max_attempts, 1);
300 }
301}