use std::io;
use std::path::PathBuf;
use thiserror::Error;
#[derive(Error, Debug, Clone)]
pub enum CliError {
#[error("I/O error for '{path}': {message}")]
Io {
path: PathBuf,
message: String,
},
#[error(
"File '{path}' is too large ({actual} bytes). Maximum allowed: {max} bytes ({max_mb} MB)"
)]
FileTooLarge {
path: PathBuf,
actual: u64,
max: u64,
max_mb: u64,
},
#[error("I/O operation timed out for '{path}' after {timeout_secs} seconds")]
IoTimeout {
path: PathBuf,
timeout_secs: u64,
},
#[error("Parse error: {0}")]
Parse(String),
#[error("Canonicalization error: {0}")]
Canonicalization(String),
#[error("JSON conversion error: {0}")]
JsonConversion(String),
#[error("JSON format error: {message}")]
JsonFormat {
message: String,
},
#[error("YAML conversion error: {0}")]
YamlConversion(String),
#[error("XML conversion error: {0}")]
XmlConversion(String),
#[error("CSV conversion error: {0}")]
CsvConversion(String),
#[error("Parquet conversion error: {0}")]
ParquetConversion(String),
#[error("Lint errors found")]
LintErrors,
#[error("File is not in canonical form")]
NotCanonical,
#[error("Invalid input: {0}")]
InvalidInput(String),
#[error("Failed to create thread pool: {message}")]
ThreadPoolError {
message: String,
requested_threads: usize,
},
#[error("Invalid glob pattern '{pattern}': {message}")]
GlobPattern {
pattern: String,
message: String,
},
#[error("File discovery failed: no files matched patterns: {}", patterns.join(", "))]
NoFilesMatched {
patterns: Vec<String>,
},
#[error("Failed to traverse directory '{path}': {message}")]
DirectoryTraversal {
path: PathBuf,
message: String,
},
#[error("Resource exhaustion: {resource_type} - {message} (usage: {current_usage}/{limit})")]
ResourceExhaustion {
resource_type: String,
message: String,
current_usage: u64,
limit: u64,
},
}
impl CliError {
pub fn io_error(path: impl Into<PathBuf>, source: io::Error) -> Self {
Self::Io {
path: path.into(),
message: source.to_string(),
}
}
pub fn file_too_large(path: impl Into<PathBuf>, actual: u64, max: u64) -> Self {
Self::FileTooLarge {
path: path.into(),
actual,
max,
max_mb: max / (1024 * 1024),
}
}
pub fn io_timeout(path: impl Into<PathBuf>, timeout_secs: u64) -> Self {
Self::IoTimeout {
path: path.into(),
timeout_secs,
}
}
pub fn parse(msg: impl Into<String>) -> Self {
Self::Parse(msg.into())
}
pub fn canonicalization(msg: impl Into<String>) -> Self {
Self::Canonicalization(msg.into())
}
pub fn invalid_input(msg: impl Into<String>) -> Self {
Self::InvalidInput(msg.into())
}
pub fn json_conversion(msg: impl Into<String>) -> Self {
Self::JsonConversion(msg.into())
}
pub fn yaml_conversion(msg: impl Into<String>) -> Self {
Self::YamlConversion(msg.into())
}
pub fn xml_conversion(msg: impl Into<String>) -> Self {
Self::XmlConversion(msg.into())
}
pub fn csv_conversion(msg: impl Into<String>) -> Self {
Self::CsvConversion(msg.into())
}
pub fn parquet_conversion(msg: impl Into<String>) -> Self {
Self::ParquetConversion(msg.into())
}
pub fn thread_pool_error(msg: impl Into<String>, requested_threads: usize) -> Self {
Self::ThreadPoolError {
message: msg.into(),
requested_threads,
}
}
#[must_use]
pub fn similar_to(&self, other: &CliError) -> bool {
std::mem::discriminant(self) == std::mem::discriminant(other)
}
#[must_use]
pub fn category(&self) -> ErrorCategory {
match self {
CliError::Io { .. } | CliError::FileTooLarge { .. } | CliError::IoTimeout { .. } => {
ErrorCategory::IoError
}
CliError::Parse(_) => ErrorCategory::ParseError,
CliError::Canonicalization(_) | CliError::NotCanonical => ErrorCategory::FormatError,
CliError::LintErrors => ErrorCategory::LintError,
CliError::GlobPattern { .. }
| CliError::NoFilesMatched { .. }
| CliError::DirectoryTraversal { .. } => ErrorCategory::FileDiscoveryError,
CliError::ResourceExhaustion { .. } | CliError::ThreadPoolError { .. } => {
ErrorCategory::ResourceError
}
CliError::JsonConversion(_)
| CliError::JsonFormat { .. }
| CliError::YamlConversion(_)
| CliError::XmlConversion(_)
| CliError::CsvConversion(_)
| CliError::ParquetConversion(_) => ErrorCategory::ConversionError,
CliError::InvalidInput(_) => ErrorCategory::ValidationError,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ErrorCategory {
IoError,
ParseError,
FormatError,
LintError,
FileDiscoveryError,
ResourceError,
ConversionError,
ValidationError,
}
impl From<serde_json::Error> for CliError {
fn from(source: serde_json::Error) -> Self {
Self::JsonFormat {
message: source.to_string(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_io_error_display() {
let err = CliError::io_error(
"test.hedl",
io::Error::new(io::ErrorKind::NotFound, "file not found"),
);
let msg = err.to_string();
assert!(msg.contains("test.hedl"));
assert!(msg.contains("file not found"));
}
#[test]
fn test_file_too_large_display() {
let err = CliError::file_too_large("big.hedl", 200_000_000, 100 * 1024 * 1024);
let msg = err.to_string();
assert!(msg.contains("big.hedl"));
assert!(msg.contains("200000000 bytes"));
assert!(msg.contains("100 MB"));
}
#[test]
fn test_io_timeout_display() {
let err = CliError::io_timeout("/slow/file.hedl", 30);
let msg = err.to_string();
assert!(msg.contains("/slow/file.hedl"));
assert!(msg.contains("30 seconds"));
}
#[test]
fn test_parse_error_display() {
let err = CliError::parse("unexpected token");
assert_eq!(err.to_string(), "Parse error: unexpected token");
}
#[test]
fn test_invalid_input_display() {
let err = CliError::invalid_input("CSV file is empty");
assert_eq!(err.to_string(), "Invalid input: CSV file is empty");
}
#[test]
fn test_json_format_error_conversion() {
let json_err = serde_json::from_str::<serde_json::Value>("invalid json").unwrap_err();
let cli_err: CliError = json_err.into();
assert!(matches!(cli_err, CliError::JsonFormat { .. }));
}
#[test]
fn test_error_cloning() {
let err = CliError::io_error(
"test.hedl",
io::Error::new(io::ErrorKind::NotFound, "not found"),
);
let cloned = err.clone();
assert_eq!(err.to_string(), cloned.to_string());
}
}