use std::fmt;
pub type RecoverableResult<T> = Result<T, RecoverableError>;
#[derive(Debug, Clone)]
pub struct RecoverableError {
pub message: String,
pub category: ErrorCategory,
pub retryable: bool,
pub guidance: Option<String>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
Git,
FileSystem,
Config,
Render,
Network,
Unknown,
}
impl RecoverableError {
pub fn new(message: impl Into<String>, category: ErrorCategory) -> Self {
Self {
message: message.into(),
category,
retryable: false,
guidance: None,
}
}
pub fn retryable(mut self) -> Self {
self.retryable = true;
self
}
pub fn with_guidance(mut self, guidance: impl Into<String>) -> Self {
self.guidance = Some(guidance.into());
self
}
pub fn git(message: impl Into<String>) -> Self {
Self::new(message, ErrorCategory::Git).retryable()
}
pub fn fs(message: impl Into<String>) -> Self {
Self::new(message, ErrorCategory::FileSystem)
}
pub fn config(message: impl Into<String>) -> Self {
Self::new(message, ErrorCategory::Config)
}
pub fn to_msg(&self) -> crate::core::Msg {
crate::core::Msg::SetError {
error: Some(self.message.clone()),
guidance: self.guidance.clone(),
}
}
}
impl fmt::Display for RecoverableError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.message)?;
if let Some(guidance) = &self.guidance {
write!(f, " ({})", guidance)?;
}
Ok(())
}
}
impl std::error::Error for RecoverableError {}
impl From<std::io::Error> for RecoverableError {
fn from(err: std::io::Error) -> Self {
Self::fs(err.to_string())
}
}
impl From<anyhow::Error> for RecoverableError {
fn from(err: anyhow::Error) -> Self {
Self::new(err.to_string(), ErrorCategory::Unknown)
}
}
pub fn retry<T, E, F>(mut f: F, max_attempts: usize) -> Result<T, E>
where
F: FnMut() -> Result<T, E>,
{
let mut attempt = 0;
loop {
match f() {
Ok(v) => return Ok(v),
Err(e) if attempt + 1 >= max_attempts => return Err(e),
Err(_) => {
attempt += 1;
std::thread::sleep(std::time::Duration::from_millis(100 * (1 << attempt)));
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let err = RecoverableError::git("Command failed")
.with_guidance("Try running git status manually");
assert_eq!(err.category, ErrorCategory::Git);
assert!(err.retryable);
assert!(err.guidance.is_some());
}
#[test]
fn test_retry_success() {
let mut counter = 0;
let result: Result<i32, &str> = retry(|| {
counter += 1;
if counter < 3 {
Err("not yet")
} else {
Ok(42)
}
}, 5);
assert_eq!(result, Ok(42));
assert_eq!(counter, 3);
}
}