use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum RoboSyncError {
#[error("I/O error: {message}")]
Io {
message: String,
#[source]
source: io::Error,
path: Option<PathBuf>,
},
#[error("Permission denied accessing {path}")]
Permission {
path: PathBuf,
#[source]
source: io::Error,
},
#[error("File or directory not found: {path}")]
NotFound { path: PathBuf },
#[error("Synchronization failed: {reason}")]
SyncFailed {
reason: String,
source_path: Option<PathBuf>,
dest_path: Option<PathBuf>,
},
#[error("Delta transfer failed: {message}")]
DeltaFailed { message: String, file_path: PathBuf },
#[error("Compression error: {operation} failed")]
Compression {
operation: String, #[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Checksum mismatch for {path}: expected {expected}, got {actual}")]
ChecksumMismatch {
path: PathBuf,
expected: String,
actual: String,
},
#[error("Strategy selection failed: {reason}")]
StrategyError {
reason: String,
attempted_strategy: Option<String>,
},
#[error("Parallel processing error: {message}")]
ParallelError {
message: String,
thread_count: Option<usize>,
},
#[error("Configuration error: {field} is invalid")]
ConfigError {
field: String,
value: String,
reason: String,
},
#[error("Network error during sync: {message}")]
Network {
message: String,
#[source]
source: Box<dyn std::error::Error + Send + Sync>,
},
#[error("Operation failed after {attempts} retries: {last_error}")]
RetryExhausted {
attempts: u32,
last_error: String,
operation: String,
},
#[error("Pattern processing error: {operation} failed")]
PatternError {
operation: String, path: Option<PathBuf>,
reason: String,
},
#[error("Operation failed: {operation} - {message}")]
OperationFailed { operation: String, message: String },
}
impl RoboSyncError {
pub fn io_error(source: io::Error, path: Option<PathBuf>) -> Self {
Self::Io {
message: source.to_string(),
source,
path,
}
}
pub fn permission_error(source: io::Error, path: PathBuf) -> Self {
Self::Permission { path, source }
}
pub fn not_found(path: PathBuf) -> Self {
Self::NotFound { path }
}
pub fn sync_failed(
reason: impl Into<String>,
source_path: Option<PathBuf>,
dest_path: Option<PathBuf>,
) -> Self {
Self::SyncFailed {
reason: reason.into(),
source_path,
dest_path,
}
}
pub fn delta_failed(message: impl Into<String>, file_path: PathBuf) -> Self {
Self::DeltaFailed {
message: message.into(),
file_path,
}
}
pub fn compression_error(
operation: impl Into<String>,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::Compression {
operation: operation.into(),
source,
}
}
pub fn checksum_mismatch(
path: PathBuf,
expected: impl Into<String>,
actual: impl Into<String>,
) -> Self {
Self::ChecksumMismatch {
path,
expected: expected.into(),
actual: actual.into(),
}
}
pub fn strategy_error(reason: impl Into<String>, attempted_strategy: Option<String>) -> Self {
Self::StrategyError {
reason: reason.into(),
attempted_strategy,
}
}
pub fn parallel_error(message: impl Into<String>, thread_count: Option<usize>) -> Self {
Self::ParallelError {
message: message.into(),
thread_count,
}
}
pub fn config_error(
field: impl Into<String>,
value: impl Into<String>,
reason: impl Into<String>,
) -> Self {
Self::ConfigError {
field: field.into(),
value: value.into(),
reason: reason.into(),
}
}
pub fn network_error(
message: impl Into<String>,
source: Box<dyn std::error::Error + Send + Sync>,
) -> Self {
Self::Network {
message: message.into(),
source,
}
}
pub fn retry_exhausted(
attempts: u32,
last_error: impl Into<String>,
operation: impl Into<String>,
) -> Self {
Self::RetryExhausted {
attempts,
last_error: last_error.into(),
operation: operation.into(),
}
}
pub fn pattern_error(
operation: impl Into<String>,
path: Option<PathBuf>,
reason: impl Into<String>,
) -> Self {
Self::PatternError {
operation: operation.into(),
path,
reason: reason.into(),
}
}
pub fn operation_failed(operation: impl Into<String>, message: impl Into<String>) -> Self {
Self::OperationFailed {
operation: operation.into(),
message: message.into(),
}
}
pub fn serialization(
context: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::operation_failed("serialization", format!("{}: {}", context.into(), source))
}
pub fn deserialization(
context: impl Into<String>,
source: impl std::error::Error + Send + Sync + 'static,
) -> Self {
Self::operation_failed("deserialization", format!("{}: {}", context.into(), source))
}
}
pub type Result<T> = std::result::Result<T, RoboSyncError>;
impl From<io::Error> for RoboSyncError {
fn from(error: io::Error) -> Self {
Self::io_error(error, None)
}
}
impl From<anyhow::Error> for RoboSyncError {
fn from(error: anyhow::Error) -> Self {
Self::operation_failed("anyhow_conversion", error.to_string())
}
}
impl From<serde_json::Error> for RoboSyncError {
fn from(error: serde_json::Error) -> Self {
Self::operation_failed("json_serialization", error.to_string())
}
}
pub trait IoErrorExt<T> {
fn with_path(self, path: PathBuf) -> Result<T>;
fn with_permission_context(self, path: PathBuf) -> Result<T>;
}
pub trait ErrorContext<T> {
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String;
}
impl<T, E> ErrorContext<T> for std::result::Result<T, E>
where
E: std::error::Error + Send + Sync + 'static,
{
fn with_context<F>(self, f: F) -> Result<T>
where
F: FnOnce() -> String,
{
self.map_err(|e| {
RoboSyncError::operation_failed("context_operation", format!("{}: {}", f(), e))
})
}
}
impl<T> IoErrorExt<T> for std::result::Result<T, io::Error> {
fn with_path(self, path: PathBuf) -> Result<T> {
self.map_err(|e| match e.kind() {
io::ErrorKind::NotFound => RoboSyncError::not_found(path),
io::ErrorKind::PermissionDenied => RoboSyncError::permission_error(e, path),
_ => RoboSyncError::io_error(e, Some(path)),
})
}
fn with_permission_context(self, path: PathBuf) -> Result<T> {
self.map_err(|e| RoboSyncError::permission_error(e, path))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_creation() {
let path = PathBuf::from("/test/path");
let io_err = io::Error::new(io::ErrorKind::NotFound, "file not found");
let error = RoboSyncError::io_error(io_err, Some(path.clone()));
assert!(matches!(error, RoboSyncError::Io { .. }));
let error = RoboSyncError::not_found(path.clone());
assert!(matches!(error, RoboSyncError::NotFound { .. }));
let error = RoboSyncError::sync_failed("test failure", Some(path.clone()), None);
assert!(matches!(error, RoboSyncError::SyncFailed { .. }));
}
#[test]
fn test_io_error_ext() {
let path = PathBuf::from("/test/path");
let result: std::result::Result<i32, io::Error> = Ok(42);
assert_eq!(
result.with_path(path.clone()).expect("Result should be Ok"),
42
);
let io_err = io::Error::new(io::ErrorKind::NotFound, "not found");
let result: std::result::Result<i32, io::Error> = Err(io_err);
let robosync_err = result.with_path(path.clone()).unwrap_err();
assert!(matches!(robosync_err, RoboSyncError::NotFound { .. }));
}
#[test]
fn test_error_display() {
let path = PathBuf::from("/test/file.txt");
let error = RoboSyncError::checksum_mismatch(path, "abc123", "def456");
let display = format!("{}", error);
assert!(display.contains("Checksum mismatch"));
assert!(display.contains("/test/file.txt"));
assert!(display.contains("abc123"));
assert!(display.contains("def456"));
}
}