use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Network error: {0}")]
Network(#[from] reqwest::Error),
#[error("Parse error: {0}")]
Parse(String),
#[error("Index error: {0}")]
Index(String),
#[error("Storage error: {0}")]
Storage(String),
#[error("Configuration error: {0}")]
Config(String),
#[error("Not found: {0}")]
NotFound(String),
#[error("Invalid URL: {0}")]
InvalidUrl(String),
#[error("Resource limited: {0}")]
ResourceLimited(String),
#[error("Timeout: {0}")]
Timeout(String),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("{0}")]
Other(String),
}
impl From<serde_json::Error> for Error {
fn from(err: serde_json::Error) -> Self {
Self::Serialization(err.to_string())
}
}
impl From<toml::ser::Error> for Error {
fn from(err: toml::ser::Error) -> Self {
Self::Serialization(err.to_string())
}
}
impl From<toml::de::Error> for Error {
fn from(err: toml::de::Error) -> Self {
Self::Serialization(err.to_string())
}
}
impl Error {
#[must_use]
pub fn is_recoverable(&self) -> bool {
match self {
Self::Network(e) => {
e.is_timeout() || e.is_connect()
},
Self::Timeout(_) => true,
Self::Io(e) => {
matches!(
e.kind(),
std::io::ErrorKind::TimedOut | std::io::ErrorKind::Interrupted
)
},
_ => false,
}
}
#[must_use]
pub const fn category(&self) -> &'static str {
match self {
Self::Io(_) => "io",
Self::Network(_) => "network",
Self::Parse(_) => "parse",
Self::Index(_) => "index",
Self::Storage(_) => "storage",
Self::Config(_) => "config",
Self::NotFound(_) => "not_found",
Self::InvalidUrl(_) => "invalid_url",
Self::ResourceLimited(_) => "resource_limited",
Self::Timeout(_) => "timeout",
Self::Serialization(_) => "serialization",
Self::Other(_) => "other",
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
#[allow(
clippy::panic,
clippy::disallowed_macros,
clippy::unwrap_used,
clippy::unnecessary_wraps
)]
mod tests {
use super::*;
use proptest::prelude::*;
use std::io;
#[test]
fn test_error_display_formatting() {
let errors = vec![
Error::Parse("invalid syntax".to_string()),
Error::Index("search failed".to_string()),
Error::Storage("disk full".to_string()),
Error::Config("missing field".to_string()),
Error::NotFound("document".to_string()),
Error::InvalidUrl("not a url".to_string()),
Error::ResourceLimited("too many requests".to_string()),
Error::Timeout("operation timed out".to_string()),
Error::Other("unknown error".to_string()),
];
for error in errors {
let error_string = error.to_string();
assert!(!error_string.is_empty());
match error {
Error::Parse(msg) => {
assert!(error_string.contains("Parse error"));
assert!(error_string.contains(&msg));
},
Error::Index(msg) => {
assert!(error_string.contains("Index error"));
assert!(error_string.contains(&msg));
},
Error::Storage(msg) => {
assert!(error_string.contains("Storage error"));
assert!(error_string.contains(&msg));
},
Error::Config(msg) => {
assert!(error_string.contains("Configuration error"));
assert!(error_string.contains(&msg));
},
Error::NotFound(msg) => {
assert!(error_string.contains("Not found"));
assert!(error_string.contains(&msg));
},
Error::InvalidUrl(msg) => {
assert!(error_string.contains("Invalid URL"));
assert!(error_string.contains(&msg));
},
Error::ResourceLimited(msg) => {
assert!(error_string.contains("Resource limited"));
assert!(error_string.contains(&msg));
},
Error::Timeout(msg) => {
assert!(error_string.contains("Timeout"));
assert!(error_string.contains(&msg));
},
Error::Other(msg) => {
assert_eq!(error_string, msg);
},
_ => {},
}
}
}
#[test]
fn test_error_from_io_error() {
let io_errors = vec![
io::Error::new(io::ErrorKind::NotFound, "file not found"),
io::Error::new(io::ErrorKind::PermissionDenied, "access denied"),
io::Error::new(io::ErrorKind::TimedOut, "operation timed out"),
io::Error::new(io::ErrorKind::Interrupted, "interrupted"),
];
for io_err in io_errors {
let error: Error = io_err.into();
match error {
Error::Io(inner) => {
assert!(!inner.to_string().is_empty());
},
_ => panic!("Expected IO error variant"),
}
}
}
#[test]
fn test_error_from_reqwest_error() {
fn create_network_error_result() -> Result<()> {
let _client = reqwest::Client::new();
Ok(())
}
assert!(create_network_error_result().is_ok());
}
#[test]
fn test_error_categories() {
let error_categories = vec![
(Error::Io(io::Error::other("test")), "io"),
(Error::Parse("test".to_string()), "parse"),
(Error::Index("test".to_string()), "index"),
(Error::Storage("test".to_string()), "storage"),
(Error::Config("test".to_string()), "config"),
(Error::NotFound("test".to_string()), "not_found"),
(Error::InvalidUrl("test".to_string()), "invalid_url"),
(
Error::ResourceLimited("test".to_string()),
"resource_limited",
),
(Error::Timeout("test".to_string()), "timeout"),
(Error::Serialization("test".to_string()), "serialization"),
(Error::Other("test".to_string()), "other"),
];
for (error, expected_category) in error_categories {
let category = error.category();
assert_eq!(category, expected_category);
}
}
#[test]
fn test_error_recoverability() {
let recoverable_errors = vec![
Error::Io(io::Error::new(io::ErrorKind::TimedOut, "timeout")),
Error::Io(io::Error::new(io::ErrorKind::Interrupted, "interrupted")),
Error::Timeout("request timeout".to_string()),
];
let non_recoverable_errors = vec![
Error::Io(io::Error::new(io::ErrorKind::NotFound, "not found")),
Error::Io(io::Error::new(io::ErrorKind::PermissionDenied, "denied")),
Error::Parse("bad syntax".to_string()),
Error::Index("corrupt index".to_string()),
Error::Storage("disk failure".to_string()),
Error::Config("invalid config".to_string()),
Error::NotFound("missing".to_string()),
Error::InvalidUrl("bad url".to_string()),
Error::ResourceLimited("quota exceeded".to_string()),
Error::Other("generic error".to_string()),
];
for error in recoverable_errors {
assert!(
error.is_recoverable(),
"Expected {error:?} to be recoverable"
);
}
for error in non_recoverable_errors {
assert!(
!error.is_recoverable(),
"Expected {error:?} to be non-recoverable"
);
}
}
#[test]
fn test_error_debug_formatting() {
let error = Error::Parse("Failed to parse JSON at line 42".to_string());
let debug_str = format!("{error:?}");
assert!(debug_str.contains("Parse"));
assert!(debug_str.contains("Failed to parse JSON at line 42"));
}
#[test]
fn test_error_chain_source() {
let io_error = io::Error::new(io::ErrorKind::PermissionDenied, "access denied");
let blz_error: Error = io_error.into();
let source = std::error::Error::source(&blz_error);
assert!(source.is_some());
let source_str = source.unwrap().to_string();
assert!(source_str.contains("access denied"));
}
#[test]
fn test_result_type_alias() {
fn test_function() -> Result<i32> {
Ok(42)
}
fn test_error_function() -> Result<i32> {
Err(Error::Other("test error".to_string()))
}
let ok_result = test_function();
let err_result = test_error_function();
assert!(ok_result.is_ok());
assert_eq!(ok_result.unwrap(), 42);
assert!(err_result.is_err());
if let Err(Error::Other(msg)) = err_result {
assert_eq!(msg, "test error");
} else {
panic!("Expected Other error");
}
}
proptest! {
#[test]
fn test_parse_error_with_arbitrary_messages(msg in r".{0,1000}") {
let error = Error::Parse(msg.clone());
let error_string = error.to_string();
prop_assert!(error_string.contains("Parse error"));
prop_assert!(error_string.contains(&msg));
prop_assert_eq!(error.category(), "parse");
prop_assert!(!error.is_recoverable());
}
#[test]
fn test_index_error_with_arbitrary_messages(msg in r".{0,1000}") {
let error = Error::Index(msg.clone());
let error_string = error.to_string();
prop_assert!(error_string.contains("Index error"));
prop_assert!(error_string.contains(&msg));
prop_assert_eq!(error.category(), "index");
prop_assert!(!error.is_recoverable());
}
#[test]
fn test_storage_error_with_arbitrary_messages(msg in r".{0,1000}") {
let error = Error::Storage(msg.clone());
let error_string = error.to_string();
prop_assert!(error_string.contains("Storage error"));
prop_assert!(error_string.contains(&msg));
prop_assert_eq!(error.category(), "storage");
prop_assert!(!error.is_recoverable());
}
#[test]
fn test_config_error_with_arbitrary_messages(msg in r".{0,1000}") {
let error = Error::Config(msg.clone());
let error_string = error.to_string();
prop_assert!(error_string.contains("Configuration error"));
prop_assert!(error_string.contains(&msg));
prop_assert_eq!(error.category(), "config");
prop_assert!(!error.is_recoverable());
}
#[test]
fn test_other_error_with_arbitrary_messages(msg in r".{0,1000}") {
let error = Error::Other(msg.clone());
let error_string = error.to_string();
prop_assert_eq!(error_string, msg);
prop_assert_eq!(error.category(), "other");
prop_assert!(!error.is_recoverable());
}
}
#[test]
fn test_error_with_malicious_messages() {
let long_message = "very_long_message_".repeat(1000);
let malicious_messages = vec![
"\n\r\x00\x01malicious",
"<script>alert('xss')</script>",
"'; DROP TABLE users; --",
"../../../etc/passwd",
"\u{202e}reverse text\u{202d}",
&long_message,
];
for malicious_msg in malicious_messages {
let errors = vec![
Error::Parse(malicious_msg.to_string()),
Error::Index(malicious_msg.to_string()),
Error::Storage(malicious_msg.to_string()),
Error::Config(malicious_msg.to_string()),
Error::NotFound(malicious_msg.to_string()),
Error::InvalidUrl(malicious_msg.to_string()),
Error::ResourceLimited(malicious_msg.to_string()),
Error::Timeout(malicious_msg.to_string()),
Error::Other(malicious_msg.to_string()),
];
for error in errors {
let error_string = error.to_string();
assert!(!error_string.is_empty());
assert!(error_string.contains(malicious_msg));
}
}
}
#[test]
fn test_error_with_unicode_messages() {
let unicode_messages = vec![
"エラーが発生しました", "حدث خطأ", "Произошла ошибка", "🚨 Błąd krytyczny! 🚨", "Error: файл не найден", ];
for unicode_msg in unicode_messages {
let error = Error::Parse(unicode_msg.to_string());
let error_string = error.to_string();
assert!(error_string.contains(unicode_msg));
assert_eq!(error.category(), "parse");
}
}
#[test]
fn test_error_empty_messages() {
let errors_with_empty_msgs = vec![
Error::Parse(String::new()),
Error::Index(String::new()),
Error::Storage(String::new()),
Error::Config(String::new()),
Error::NotFound(String::new()),
Error::InvalidUrl(String::new()),
Error::ResourceLimited(String::new()),
Error::Timeout(String::new()),
Error::Other(String::new()),
];
for error in errors_with_empty_msgs {
let error_string = error.to_string();
if let Error::Other(_) = error {
assert_eq!(error_string, "");
} else {
assert!(!error_string.is_empty());
assert!(
error_string.contains(':'),
"Error should contain colon separator: '{error_string}'"
);
}
}
}
#[test]
fn test_error_size() {
let error_size = std::mem::size_of::<Error>();
assert!(error_size <= 64, "Error type too large: {error_size} bytes");
}
}