use anyhow::Result;
use std::time::Duration;
use tokio::time::sleep;
#[derive(Debug, Clone)]
pub struct RetryPolicy {
pub max_attempts: u32,
pub initial_delay: Duration,
pub backoff_multiplier: f64,
pub max_delay: Duration,
}
impl Default for RetryPolicy {
fn default() -> Self {
Self {
max_attempts: 3,
initial_delay: Duration::from_millis(100),
backoff_multiplier: 2.0,
max_delay: Duration::from_secs(5),
}
}
}
impl RetryPolicy {
pub fn no_retry() -> Self {
Self {
max_attempts: 1,
..Default::default()
}
}
pub fn aggressive() -> Self {
Self {
max_attempts: 5,
initial_delay: Duration::from_millis(50),
backoff_multiplier: 1.5,
max_delay: Duration::from_secs(3),
}
}
}
pub async fn with_retry<F, T, E>(policy: &RetryPolicy, mut operation: F) -> Result<T>
where
F: FnMut() -> Result<T, E>,
E: std::fmt::Display,
{
let mut attempts = 0;
let mut delay = policy.initial_delay;
loop {
attempts += 1;
match operation() {
Ok(result) => return Ok(result),
Err(e) => {
if attempts >= policy.max_attempts {
return Err(anyhow::anyhow!(
"Operation failed after {} attempts: {}",
attempts,
e
));
}
eprintln!(
"⚠️ Attempt {}/{} failed: {}. Retrying in {:?}...",
attempts, policy.max_attempts, e, delay
);
sleep(delay).await;
delay = Duration::from_secs_f64(
(delay.as_secs_f64() * policy.backoff_multiplier).min(policy.max_delay.as_secs_f64())
);
}
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorCategory {
Network,
FileSystem,
Configuration,
Validation,
Dependency,
Timeout,
Unknown,
}
impl ErrorCategory {
pub fn is_retryable(&self) -> bool {
matches!(
self,
ErrorCategory::Network | ErrorCategory::FileSystem | ErrorCategory::Timeout
)
}
}
pub fn categorize_error(error: &anyhow::Error) -> ErrorCategory {
let error_str = error.to_string().to_lowercase();
if error_str.contains("network")
|| error_str.contains("connection")
|| error_str.contains("timeout")
|| error_str.contains("dns")
{
ErrorCategory::Network
} else if error_str.contains("file")
|| error_str.contains("directory")
|| error_str.contains("permission")
|| error_str.contains("io")
{
ErrorCategory::FileSystem
} else if error_str.contains("config") || error_str.contains("invalid") {
ErrorCategory::Configuration
} else if error_str.contains("dependency") || error_str.contains("version") {
ErrorCategory::Dependency
} else if error_str.contains("timeout") {
ErrorCategory::Timeout
} else {
ErrorCategory::Unknown
}
}
#[derive(Debug)]
pub struct EnhancedError {
pub error: anyhow::Error,
pub category: ErrorCategory,
pub context: Vec<String>,
pub suggestions: Vec<String>,
}
impl EnhancedError {
pub fn new(error: anyhow::Error) -> Self {
let category = categorize_error(&error);
let (context, suggestions) = generate_context_and_suggestions(&category, &error);
Self {
error,
category,
context,
suggestions,
}
}
pub fn display(&self) -> String {
let mut output = format!("❌ Error: {}\n", self.error);
if !self.context.is_empty() {
output.push_str("\n📋 Context:\n");
for ctx in &self.context {
output.push_str(&format!(" • {}\n", ctx));
}
}
if !self.suggestions.is_empty() {
output.push_str("\n💡 Suggestions:\n");
for suggestion in &self.suggestions {
output.push_str(&format!(" • {}\n", suggestion));
}
}
output
}
}
fn generate_context_and_suggestions(
category: &ErrorCategory,
error: &anyhow::Error,
) -> (Vec<String>, Vec<String>) {
let mut context = Vec::new();
let mut suggestions = Vec::new();
match category {
ErrorCategory::Network => {
context.push("Network operation failed".to_string());
suggestions.push("Check your internet connection".to_string());
suggestions.push("Verify firewall settings".to_string());
suggestions.push("Try again in a few moments".to_string());
}
ErrorCategory::FileSystem => {
context.push("File system operation failed".to_string());
suggestions.push("Check file permissions".to_string());
suggestions.push("Verify the path exists".to_string());
suggestions.push("Ensure sufficient disk space".to_string());
}
ErrorCategory::Configuration => {
context.push("Configuration error detected".to_string());
suggestions.push("Review your configuration file".to_string());
suggestions.push("Check environment variables".to_string());
suggestions.push("Refer to documentation for valid options".to_string());
}
ErrorCategory::Dependency => {
context.push("Dependency resolution failed".to_string());
suggestions.push("Check tool dependencies".to_string());
suggestions.push("Verify version compatibility".to_string());
suggestions.push("Run 'forge update' to sync dependencies".to_string());
}
ErrorCategory::Timeout => {
context.push("Operation timed out".to_string());
suggestions.push("The operation may need more time".to_string());
suggestions.push("Try increasing timeout settings".to_string());
suggestions.push("Check system resources".to_string());
}
ErrorCategory::Validation => {
context.push("Validation error".to_string());
suggestions.push("Review input data".to_string());
suggestions.push("Check for required fields".to_string());
}
ErrorCategory::Unknown => {
context.push(format!("Unexpected error: {}", error));
suggestions.push("Check logs for more details".to_string());
suggestions.push("Report this issue if it persists".to_string());
}
}
(context, suggestions)
}
pub type EnhancedResult<T> = Result<T, EnhancedError>;
pub trait ToEnhanced<T> {
fn enhance(self) -> EnhancedResult<T>;
}
impl<T, E: Into<anyhow::Error>> ToEnhanced<T> for Result<T, E> {
fn enhance(self) -> EnhancedResult<T> {
self.map_err(|e| EnhancedError::new(e.into()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_categorization() {
let net_err = anyhow::anyhow!("Network connection failed");
assert_eq!(categorize_error(&net_err), ErrorCategory::Network);
let fs_err = anyhow::anyhow!("File not found");
assert_eq!(categorize_error(&fs_err), ErrorCategory::FileSystem);
let config_err = anyhow::anyhow!("Invalid config value");
assert_eq!(categorize_error(&config_err), ErrorCategory::Configuration);
}
#[test]
fn test_retryable() {
assert!(ErrorCategory::Network.is_retryable());
assert!(!ErrorCategory::Configuration.is_retryable());
}
#[test]
fn test_retry_policy() {
let policy = RetryPolicy::default();
assert_eq!(policy.max_attempts, 3);
let no_retry = RetryPolicy::no_retry();
assert_eq!(no_retry.max_attempts, 1);
}
}