use thiserror::Error;
#[derive(Debug, Error)]
pub enum CsvError {
#[error("CSV parse error at line {line}: {message}")]
ParseError {
line: usize,
message: String,
},
#[error("Type mismatch in column '{column}': expected {expected}, got '{value}'")]
TypeMismatch {
column: String,
expected: String,
value: String,
},
#[error("Missing required column: {0}")]
MissingColumn(String),
#[error("Invalid header at position {position}: {reason}")]
InvalidHeader {
position: usize,
reason: String,
},
#[error("Row width mismatch: expected {expected} columns, got {actual} in row {row}")]
WidthMismatch {
expected: usize,
actual: usize,
row: usize,
},
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("CSV library error: {0}")]
CsvLib(#[from] csv::Error),
#[error("HEDL core error: {0}")]
HedlCore(String),
#[error("Security limit exceeded: row count {actual} exceeds maximum {limit}")]
SecurityLimit {
limit: usize,
actual: usize,
},
#[error("Empty 'id' field at row {row}")]
EmptyId {
row: usize,
},
#[error("Matrix list '{name}' not found in document (available: {available})")]
ListNotFound {
name: String,
available: String,
},
#[error("Item '{name}' is not a matrix list (found: {actual_type})")]
NotAList {
name: String,
actual_type: String,
},
#[error("No matrix lists found in document")]
NoLists,
#[error("Invalid UTF-8 in {context}")]
InvalidUtf8 {
context: String,
},
#[error("{0}")]
Other(String),
#[error("Security limit violated: {message}")]
Security {
limit_type: String,
limit: usize,
actual: usize,
message: String,
},
}
pub type Result<T> = std::result::Result<T, CsvError>;
impl CsvError {
#[must_use]
pub fn with_context(self, context: String) -> Self {
match self {
CsvError::ParseError { line, message } => CsvError::ParseError {
line,
message: format!("{message} ({context})"),
},
CsvError::HedlCore(msg) => CsvError::HedlCore(format!("{msg} ({context})")),
CsvError::Other(msg) => CsvError::Other(format!("{msg} ({context})")),
other => CsvError::Other(format!("{other} ({context})")),
}
}
#[must_use]
pub fn security(message: String, _line: usize) -> Self {
CsvError::Security {
limit_type: "unknown".to_string(),
limit: 0,
actual: 0,
message,
}
}
}
impl From<hedl_core::HedlError> for CsvError {
fn from(err: hedl_core::HedlError) -> Self {
CsvError::HedlCore(err.to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_error_display() {
let err = CsvError::ParseError {
line: 42,
message: "Invalid escape sequence".to_string(),
};
assert_eq!(
err.to_string(),
"CSV parse error at line 42: Invalid escape sequence"
);
}
#[test]
fn test_type_mismatch_display() {
let err = CsvError::TypeMismatch {
column: "age".to_string(),
expected: "integer".to_string(),
value: "abc".to_string(),
};
assert_eq!(
err.to_string(),
"Type mismatch in column 'age': expected integer, got 'abc'"
);
}
#[test]
fn test_missing_column_display() {
let err = CsvError::MissingColumn("id".to_string());
assert_eq!(err.to_string(), "Missing required column: id");
}
#[test]
fn test_invalid_header_display() {
let err = CsvError::InvalidHeader {
position: 3,
reason: "Empty column name".to_string(),
};
assert_eq!(
err.to_string(),
"Invalid header at position 3: Empty column name"
);
}
#[test]
fn test_width_mismatch_display() {
let err = CsvError::WidthMismatch {
expected: 5,
actual: 3,
row: 10,
};
assert_eq!(
err.to_string(),
"Row width mismatch: expected 5 columns, got 3 in row 10"
);
}
#[test]
fn test_security_limit_display() {
let err = CsvError::SecurityLimit {
limit: 1_000_000,
actual: 1_500_000,
};
assert_eq!(
err.to_string(),
"Security limit exceeded: row count 1500000 exceeds maximum 1000000"
);
}
#[test]
fn test_empty_id_display() {
let err = CsvError::EmptyId { row: 5 };
assert_eq!(err.to_string(), "Empty 'id' field at row 5");
}
#[test]
fn test_list_not_found_display() {
let err = CsvError::ListNotFound {
name: "people".to_string(),
available: "users, items".to_string(),
};
assert_eq!(
err.to_string(),
"Matrix list 'people' not found in document (available: users, items)"
);
}
#[test]
fn test_not_a_list_display() {
let err = CsvError::NotAList {
name: "value".to_string(),
actual_type: "scalar".to_string(),
};
assert_eq!(
err.to_string(),
"Item 'value' is not a matrix list (found: scalar)"
);
}
#[test]
fn test_no_lists_display() {
let err = CsvError::NoLists;
assert_eq!(err.to_string(), "No matrix lists found in document");
}
#[test]
fn test_invalid_utf8_display() {
let err = CsvError::InvalidUtf8 {
context: "CSV output".to_string(),
};
assert_eq!(err.to_string(), "Invalid UTF-8 in CSV output");
}
#[test]
fn test_other_display() {
let err = CsvError::Other("Custom error message".to_string());
assert_eq!(err.to_string(), "Custom error message");
}
#[test]
fn test_io_error_conversion() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found");
let csv_err = CsvError::from(io_err);
assert!(csv_err.to_string().contains("I/O error"));
}
#[test]
fn test_hedl_error_conversion() {
let hedl_err = hedl_core::HedlError::new(
hedl_core::HedlErrorKind::Syntax,
"Syntax error".to_string(),
1,
);
let csv_err = CsvError::from(hedl_err);
assert!(csv_err.to_string().contains("HEDL core error"));
}
#[test]
fn test_error_is_send_sync() {
fn assert_send_sync<T: Send + Sync>() {}
assert_send_sync::<CsvError>();
}
#[test]
fn test_error_debug() {
let err = CsvError::MissingColumn("id".to_string());
let debug = format!("{err:?}");
assert!(debug.contains("MissingColumn"));
assert!(debug.contains("id"));
}
#[test]
fn test_error_messages() {
let err = CsvError::TypeMismatch {
column: "age".to_string(),
expected: "integer".to_string(),
value: "abc".to_string(),
};
assert_eq!(
err.to_string(),
"Type mismatch in column 'age': expected integer, got 'abc'"
);
}
#[test]
fn test_with_context() {
let err = CsvError::ParseError {
line: 10,
message: "Invalid value".to_string(),
};
let with_ctx = err.with_context("in field 'name'".to_string());
assert_eq!(
with_ctx.to_string(),
"CSV parse error at line 10: Invalid value (in field 'name')"
);
}
#[test]
fn test_security_display() {
let err = CsvError::Security {
limit_type: "column count".to_string(),
limit: 10_000,
actual: 15_000,
message: "CSV has 15000 columns, exceeds limit of 10000".to_string(),
};
assert!(err.to_string().contains("Security limit"));
assert!(err.to_string().contains("15000"));
}
#[test]
fn test_security_error() {
let err = CsvError::security(
"CSV has 15000 columns, exceeds limit of 10000".to_string(),
0,
);
assert!(matches!(err, CsvError::Security { .. }));
assert!(err.to_string().contains("Security limit"));
}
#[test]
fn test_security_with_context() {
let err = CsvError::Security {
limit_type: "cell size".to_string(),
limit: 1_048_576,
actual: 2_000_000,
message: "Cell size exceeds limit".to_string(),
};
let with_ctx = err.with_context("at row 5, column 3".to_string());
assert!(with_ctx.to_string().contains("Cell size exceeds limit"));
assert!(with_ctx.to_string().contains("at row 5, column 3"));
}
}