use std::io;
use std::path::PathBuf;
use thiserror::Error;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Error, Debug)]
pub enum Error {
#[error("IO error: {0}")]
Io(#[from] io::Error),
#[error("Regex error: {0}")]
Regex(#[from] regex::Error),
#[error("JSON error: {0}")]
Json(#[from] serde_json::Error),
#[error("Database error: {0}")]
Database(#[from] rusqlite::Error),
#[error("Configuration file not found: {path}")]
ConfigNotFound { path: PathBuf },
#[error("History file not found: {path}")]
HistoryFileNotFound { path: PathBuf },
#[error("Invalid history file format in {path}: {reason}")]
InvalidHistoryFormat { path: PathBuf, reason: String },
#[error("Home directory not found")]
HomeDirectoryNotFound,
#[error("Invalid arguments: {message}")]
InvalidArguments { message: String },
#[error("Command not found in history")]
CommandNotFound,
#[error("Invalid timestamp format: {timestamp}")]
InvalidTimestamp { timestamp: String },
#[error("Permission denied: {path}")]
PermissionDenied { path: PathBuf },
#[error("File already exists: {path}")]
FileExists { path: PathBuf },
#[error("Invalid redaction pattern: {pattern}")]
InvalidRedactionPattern { pattern: String },
#[error("Shell integration error: {shell} - {reason}")]
ShellIntegration { shell: String, reason: String },
#[error("Import failed from {from}: {reason}")]
ImportFailed { from: String, reason: String },
#[error("Search failed: {reason}")]
SearchFailed { reason: String },
#[error("Configuration validation failed: {field} - {reason}")]
ConfigValidation { field: String, reason: String },
#[error("{message}")]
Custom { message: String },
}
impl Error {
pub fn custom<S: Into<String>>(message: S) -> Self {
Error::Custom {
message: message.into(),
}
}
pub fn invalid_arguments<S: Into<String>>(message: S) -> Self {
Error::InvalidArguments {
message: message.into(),
}
}
pub fn config_validation<S: Into<String>>(field: S, reason: S) -> Self {
Error::ConfigValidation {
field: field.into(),
reason: reason.into(),
}
}
pub fn import_failed<S: Into<String>>(from: S, reason: S) -> Self {
Error::ImportFailed {
from: from.into(),
reason: reason.into(),
}
}
pub fn search_failed<S: Into<String>>(reason: S) -> Self {
Error::SearchFailed {
reason: reason.into(),
}
}
pub fn is_recoverable(&self) -> bool {
match self {
Error::Io(_) => true,
Error::ConfigNotFound { .. } => true,
Error::HistoryFileNotFound { .. } => true,
Error::CommandNotFound => true,
Error::InvalidArguments { .. } => false,
Error::PermissionDenied { .. } => false,
Error::HomeDirectoryNotFound => false,
_ => true,
}
}
pub fn category(&self) -> &'static str {
match self {
Error::Io(_) => "io",
Error::Regex(_) => "regex",
Error::Json(_) => "json",
Error::Database(_) => "database",
Error::ConfigNotFound { .. } | Error::ConfigValidation { .. } => "config",
Error::HistoryFileNotFound { .. } | Error::InvalidHistoryFormat { .. } => "history",
Error::HomeDirectoryNotFound => "system",
Error::InvalidArguments { .. } => "arguments",
Error::CommandNotFound => "search",
Error::InvalidTimestamp { .. } => "timestamp",
Error::PermissionDenied { .. } => "permission",
Error::FileExists { .. } => "file",
Error::InvalidRedactionPattern { .. } => "redaction",
Error::ShellIntegration { .. } => "shell",
Error::ImportFailed { .. } => "import",
Error::SearchFailed { .. } => "search",
Error::Custom { .. } => "custom",
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::Path;
#[test]
fn test_custom_error() {
let err = Error::custom("test message");
assert_eq!(err.to_string(), "test message");
assert_eq!(err.category(), "custom");
}
#[test]
fn test_invalid_arguments_error() {
let err = Error::invalid_arguments("missing required argument");
assert_eq!(
err.to_string(),
"Invalid arguments: missing required argument"
);
assert_eq!(err.category(), "arguments");
assert!(!err.is_recoverable());
}
#[test]
fn test_config_validation_error() {
let err = Error::config_validation("max_entries", "must be positive");
assert_eq!(
err.to_string(),
"Configuration validation failed: max_entries - must be positive"
);
assert_eq!(err.category(), "config");
}
#[test]
fn test_history_file_not_found() {
let path = Path::new("/nonexistent/history").to_path_buf();
let err = Error::HistoryFileNotFound { path: path.clone() };
assert_eq!(
err.to_string(),
format!("History file not found: {}", path.display())
);
assert_eq!(err.category(), "history");
assert!(err.is_recoverable());
}
#[test]
fn test_permission_denied() {
let path = Path::new("/root/protected").to_path_buf();
let err = Error::PermissionDenied { path: path.clone() };
assert_eq!(
err.to_string(),
format!("Permission denied: {}", path.display())
);
assert_eq!(err.category(), "permission");
assert!(!err.is_recoverable());
}
#[test]
fn test_import_failed() {
let err = Error::import_failed("zsh", "invalid format");
assert_eq!(err.to_string(), "Import failed from zsh: invalid format");
assert_eq!(err.category(), "import");
}
#[test]
fn test_search_failed() {
let err = Error::search_failed("no matches found");
assert_eq!(err.to_string(), "Search failed: no matches found");
assert_eq!(err.category(), "search");
}
#[test]
fn test_error_recovery() {
let recoverable = Error::CommandNotFound;
assert!(recoverable.is_recoverable());
let non_recoverable = Error::HomeDirectoryNotFound;
assert!(!non_recoverable.is_recoverable());
}
}