use std::fmt;
use thiserror::Error;
use crate::format_convert::FormatConvertError;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IoOperationKind {
Read,
Write,
Create,
Delete,
Rename,
CreateDir,
ReadDir,
Sync,
}
impl fmt::Display for IoOperationKind {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Read => write!(f, "read"),
Self::Write => write!(f, "write"),
Self::Create => write!(f, "create"),
Self::Delete => write!(f, "delete"),
Self::Rename => write!(f, "rename"),
Self::CreateDir => write!(f, "create directory"),
Self::ReadDir => write!(f, "read directory"),
Self::Sync => write!(f, "sync"),
}
}
}
fn format_io_error(
operation: &IoOperationKind,
path: &str,
context: &Option<String>,
error: &str,
) -> String {
if let Some(ctx) = context {
format!("Failed to {} {} at '{}': {}", operation, ctx, path, error)
} else {
format!("Failed to {} file at '{}': {}", operation, path, error)
}
}
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum StoreError {
#[error("{}", format_io_error(.operation, .path, .context, .error))]
IoError {
operation: IoOperationKind,
path: String,
context: Option<String>,
error: String,
},
#[error("Cannot determine home directory")]
HomeDirNotFound,
#[error("Failed to encode filename for ID '{id}': {reason}")]
FilenameEncoding {
id: String,
reason: String,
},
#[error("format conversion: {0}")]
FormatConvert(#[from] FormatConvertError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_store_error_io_error_display_without_context() {
let err = StoreError::IoError {
operation: IoOperationKind::Read,
path: "/path/to/file.toml".to_string(),
context: None,
error: "Permission denied".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("Failed to read"));
assert!(display.contains("/path/to/file.toml"));
assert!(display.contains("Permission denied"));
}
#[test]
fn test_store_error_io_error_display_with_context() {
let err = StoreError::IoError {
operation: IoOperationKind::Write,
path: "/path/to/tmp.toml".to_string(),
context: Some("temporary file".to_string()),
error: "Disk full".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("Failed to write"));
assert!(display.contains("temporary file"));
assert!(display.contains("/path/to/tmp.toml"));
assert!(display.contains("Disk full"));
}
#[test]
fn test_store_error_home_dir_not_found_display() {
let err = StoreError::HomeDirNotFound;
let display = format!("{}", err);
assert!(display.contains("Cannot determine home directory"));
}
#[test]
fn test_store_error_is_std_error() {
let err = StoreError::HomeDirNotFound;
let _: &dyn std::error::Error = &err;
}
#[test]
fn test_io_operation_kind_display() {
assert_eq!(IoOperationKind::Read.to_string(), "read");
assert_eq!(IoOperationKind::Write.to_string(), "write");
assert_eq!(IoOperationKind::Create.to_string(), "create");
assert_eq!(IoOperationKind::Delete.to_string(), "delete");
assert_eq!(IoOperationKind::Rename.to_string(), "rename");
assert_eq!(IoOperationKind::CreateDir.to_string(), "create directory");
assert_eq!(IoOperationKind::ReadDir.to_string(), "read directory");
assert_eq!(IoOperationKind::Sync.to_string(), "sync");
}
#[test]
fn test_store_error_filename_encoding_display() {
let err = StoreError::FilenameEncoding {
id: "my/id".to_string(),
reason: "ID contains invalid characters for Direct encoding".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("my/id"), "display should contain id");
assert!(
display.contains("invalid characters"),
"display should contain reason"
);
}
#[test]
fn test_store_error_io_error_rename_with_retries() {
let err = StoreError::IoError {
operation: IoOperationKind::Rename,
path: "/path/to/file.toml".to_string(),
context: Some("after 3 retries".to_string()),
error: "Resource temporarily unavailable".to_string(),
};
let display = format!("{}", err);
assert!(display.contains("Failed to rename"));
assert!(display.contains("after 3 retries"));
assert!(display.contains("/path/to/file.toml"));
assert!(display.contains("Resource temporarily unavailable"));
}
}