dx_forge/
error.rs

1//! Production Error Handling and Retry Logic
2//!
3//! Provides robust error handling, retry mechanisms, and detailed error reporting
4//! for DX tools orchestration.
5
6use anyhow::Result;
7use std::time::Duration;
8use tokio::time::sleep;
9
10/// Retry policy configuration
11#[derive(Debug, Clone)]
12pub struct RetryPolicy {
13    /// Maximum number of retry attempts
14    pub max_attempts: u32,
15
16    /// Initial delay between retries
17    pub initial_delay: Duration,
18
19    /// Exponential backoff multiplier
20    pub backoff_multiplier: f64,
21
22    /// Maximum delay between retries
23    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    /// Create a no-retry policy
39    pub fn no_retry() -> Self {
40        Self {
41            max_attempts: 1,
42            ..Default::default()
43        }
44    }
45
46    /// Create an aggressive retry policy
47    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
57/// Execute with retry logic
58pub 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                // Exponential backoff
88                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/// Categorized error types for better handling
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub enum ErrorCategory {
99    /// Network-related errors (retryable)
100    Network,
101
102    /// File system errors (may be retryable)
103    FileSystem,
104
105    /// Configuration errors (not retryable)
106    Configuration,
107
108    /// Validation errors (not retryable)
109    Validation,
110
111    /// Dependency errors (not retryable)
112    Dependency,
113
114    /// Timeout errors (may be retryable)
115    Timeout,
116
117    /// Unknown errors
118    Unknown,
119}
120
121impl ErrorCategory {
122    /// Check if this error category is retryable
123    pub fn is_retryable(&self) -> bool {
124        matches!(
125            self,
126            ErrorCategory::Network | ErrorCategory::FileSystem | ErrorCategory::Timeout
127        )
128    }
129}
130
131/// Categorize an error
132pub 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/// Enhanced error with context and suggestions
159#[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    /// Create an enhanced error
169    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    /// Display the error with all context
182    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
203/// Generate helpful context and suggestions based on error category
204fn 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
257/// Result type with enhanced error
258pub type EnhancedResult<T> = Result<T, EnhancedError>;
259
260/// Convert regular Result to EnhancedResult
261pub 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}