use hwpforge_foundation::FoundationError;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum CoreError {
#[error("Document validation failed: {0}")]
Validation(#[from] ValidationError),
#[error("Foundation error: {0}")]
Foundation(#[from] FoundationError),
#[error("Invalid document structure in {context}: {reason}")]
InvalidStructure {
context: String,
reason: String,
},
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ValidationError {
#[error("Empty document: at least 1 section required")]
EmptyDocument,
#[error("Section {section_index} has no paragraphs")]
EmptySection {
section_index: usize,
},
#[error("Paragraph has no runs (section {section_index}, paragraph {paragraph_index})")]
EmptyParagraph {
section_index: usize,
paragraph_index: usize,
},
#[error(
"Table has no rows (section {section_index}, paragraph {paragraph_index}, run {run_index})"
)]
EmptyTable {
section_index: usize,
paragraph_index: usize,
run_index: usize,
},
#[error("Table row has no cells (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index})")]
EmptyTableRow {
section_index: usize,
paragraph_index: usize,
run_index: usize,
row_index: usize,
},
#[error("Invalid span: {field} = {value} (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index}, cell {cell_index})")]
InvalidSpan {
field: &'static str,
value: u16,
section_index: usize,
paragraph_index: usize,
run_index: usize,
row_index: usize,
cell_index: usize,
},
#[error("TextBox has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
EmptyTextBox {
section_index: usize,
paragraph_index: usize,
run_index: usize,
},
#[error("Footnote has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
EmptyFootnote {
section_index: usize,
paragraph_index: usize,
run_index: usize,
},
#[error("Endnote has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
EmptyEndnote {
section_index: usize,
paragraph_index: usize,
run_index: usize,
},
#[error("Polygon has invalid vertex count: {vertex_count} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
InvalidPolygon {
section_index: usize,
paragraph_index: usize,
run_index: usize,
vertex_count: usize,
},
#[error("Shape {shape_type} has zero dimension (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
InvalidShapeDimension {
section_index: usize,
paragraph_index: usize,
run_index: usize,
shape_type: &'static str,
},
#[error("Chart has empty data (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
EmptyChartData {
section_index: usize,
paragraph_index: usize,
run_index: usize,
},
#[error("Equation has empty script (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
EmptyEquation {
section_index: usize,
paragraph_index: usize,
run_index: usize,
},
#[error("Chart has empty category labels (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
EmptyCategoryLabels {
section_index: usize,
paragraph_index: usize,
run_index: usize,
},
#[error("XY series '{series_name}' has mismatched lengths: x={x_len}, y={y_len} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
MismatchedSeriesLengths {
section_index: usize,
paragraph_index: usize,
run_index: usize,
series_name: String,
x_len: usize,
y_len: usize,
},
#[error("Table cell has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index}, cell {cell_index})")]
EmptyTableCell {
section_index: usize,
paragraph_index: usize,
run_index: usize,
row_index: usize,
cell_index: usize,
},
#[error("Table header row is not part of the leading header block (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index})")]
NonLeadingTableHeaderRow {
section_index: usize,
paragraph_index: usize,
run_index: usize,
row_index: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(u32)]
pub enum CoreErrorCode {
EmptyDocument = 2000,
EmptySection = 2001,
EmptyParagraph = 2002,
EmptyTable = 2003,
EmptyTableRow = 2004,
InvalidSpan = 2005,
EmptyTextBox = 2006,
EmptyFootnote = 2007,
EmptyTableCell = 2008,
EmptyEndnote = 2009,
InvalidPolygon = 2010,
InvalidShapeDimension = 2011,
EmptyEquation = 2012,
EmptyChartData = 2013,
EmptyCategoryLabels = 2014,
MismatchedSeriesLengths = 2015,
NonLeadingTableHeaderRow = 2016,
InvalidStructure = 2100,
}
impl std::fmt::Display for CoreErrorCode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "E{:04}", *self as u32)
}
}
impl ValidationError {
pub fn code(&self) -> CoreErrorCode {
match self {
Self::EmptyDocument => CoreErrorCode::EmptyDocument,
Self::EmptySection { .. } => CoreErrorCode::EmptySection,
Self::EmptyParagraph { .. } => CoreErrorCode::EmptyParagraph,
Self::EmptyTable { .. } => CoreErrorCode::EmptyTable,
Self::EmptyTableRow { .. } => CoreErrorCode::EmptyTableRow,
Self::InvalidSpan { .. } => CoreErrorCode::InvalidSpan,
Self::NonLeadingTableHeaderRow { .. } => CoreErrorCode::NonLeadingTableHeaderRow,
Self::EmptyTextBox { .. } => CoreErrorCode::EmptyTextBox,
Self::EmptyFootnote { .. } => CoreErrorCode::EmptyFootnote,
Self::EmptyTableCell { .. } => CoreErrorCode::EmptyTableCell,
Self::EmptyEndnote { .. } => CoreErrorCode::EmptyEndnote,
Self::InvalidPolygon { .. } => CoreErrorCode::InvalidPolygon,
Self::InvalidShapeDimension { .. } => CoreErrorCode::InvalidShapeDimension,
Self::EmptyChartData { .. } => CoreErrorCode::EmptyChartData,
Self::EmptyCategoryLabels { .. } => CoreErrorCode::EmptyCategoryLabels,
Self::MismatchedSeriesLengths { .. } => CoreErrorCode::MismatchedSeriesLengths,
Self::EmptyEquation { .. } => CoreErrorCode::EmptyEquation,
}
}
}
pub type CoreResult<T> = Result<T, CoreError>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_document_displays_message() {
let err = ValidationError::EmptyDocument;
let msg = err.to_string();
assert!(msg.contains("section"), "msg: {msg}");
assert!(msg.contains("at least 1"), "msg: {msg}");
}
#[test]
fn empty_section_displays_index() {
let err = ValidationError::EmptySection { section_index: 3 };
let msg = err.to_string();
assert!(msg.contains("3"), "msg: {msg}");
assert!(msg.contains("no paragraphs"), "msg: {msg}");
}
#[test]
fn empty_paragraph_displays_location() {
let err = ValidationError::EmptyParagraph { section_index: 1, paragraph_index: 5 };
let msg = err.to_string();
assert!(msg.contains("section 1"), "msg: {msg}");
assert!(msg.contains("paragraph 5"), "msg: {msg}");
}
#[test]
fn empty_table_displays_location() {
let err =
ValidationError::EmptyTable { section_index: 0, paragraph_index: 2, run_index: 0 };
let msg = err.to_string();
assert!(msg.contains("no rows"), "msg: {msg}");
}
#[test]
fn empty_table_row_displays_location() {
let err = ValidationError::EmptyTableRow {
section_index: 0,
paragraph_index: 0,
run_index: 0,
row_index: 1,
};
let msg = err.to_string();
assert!(msg.contains("row 1"), "msg: {msg}");
assert!(msg.contains("no cells"), "msg: {msg}");
}
#[test]
fn invalid_span_displays_all_context() {
let err = ValidationError::InvalidSpan {
field: "col_span",
value: 0,
section_index: 0,
paragraph_index: 1,
run_index: 0,
row_index: 0,
cell_index: 2,
};
let msg = err.to_string();
assert!(msg.contains("col_span"), "msg: {msg}");
assert!(msg.contains("= 0"), "msg: {msg}");
assert!(msg.contains("cell 2"), "msg: {msg}");
}
#[test]
fn empty_text_box_displays_location() {
let err =
ValidationError::EmptyTextBox { section_index: 0, paragraph_index: 0, run_index: 1 };
let msg = err.to_string();
assert!(msg.contains("TextBox"), "msg: {msg}");
}
#[test]
fn empty_footnote_displays_location() {
let err =
ValidationError::EmptyFootnote { section_index: 0, paragraph_index: 0, run_index: 0 };
let msg = err.to_string();
assert!(msg.contains("Footnote"), "msg: {msg}");
}
#[test]
fn empty_table_cell_displays_location() {
let err = ValidationError::EmptyTableCell {
section_index: 0,
paragraph_index: 0,
run_index: 0,
row_index: 0,
cell_index: 0,
};
let msg = err.to_string();
assert!(msg.contains("cell"), "msg: {msg}");
}
#[test]
fn core_error_from_validation() {
let ve = ValidationError::EmptyDocument;
let ce: CoreError = ve.into();
match ce {
CoreError::Validation(v) => assert_eq!(v, ValidationError::EmptyDocument),
other => panic!("expected Validation, got: {other}"),
}
}
#[test]
fn core_error_from_foundation() {
let fe =
FoundationError::InvalidField { field: "test".to_string(), reason: "bad".to_string() };
let ce: CoreError = fe.into();
assert!(matches!(ce, CoreError::Foundation(_)));
}
#[test]
fn core_error_invalid_structure() {
let ce = CoreError::InvalidStructure {
context: "document".to_string(),
reason: "circular reference".to_string(),
};
let msg = ce.to_string();
assert!(msg.contains("document"), "msg: {msg}");
assert!(msg.contains("circular"), "msg: {msg}");
}
#[test]
fn error_codes_in_core_range() {
assert_eq!(CoreErrorCode::EmptyDocument as u32, 2000);
assert_eq!(CoreErrorCode::EmptySection as u32, 2001);
assert_eq!(CoreErrorCode::EmptyParagraph as u32, 2002);
assert_eq!(CoreErrorCode::EmptyTable as u32, 2003);
assert_eq!(CoreErrorCode::EmptyTableRow as u32, 2004);
assert_eq!(CoreErrorCode::InvalidSpan as u32, 2005);
assert_eq!(CoreErrorCode::EmptyTextBox as u32, 2006);
assert_eq!(CoreErrorCode::EmptyFootnote as u32, 2007);
assert_eq!(CoreErrorCode::EmptyTableCell as u32, 2008);
assert_eq!(CoreErrorCode::InvalidStructure as u32, 2100);
}
#[test]
fn error_code_display_format() {
assert_eq!(CoreErrorCode::EmptyDocument.to_string(), "E2000");
assert_eq!(CoreErrorCode::InvalidStructure.to_string(), "E2100");
}
#[test]
fn validation_error_code_mapping() {
assert_eq!(ValidationError::EmptyDocument.code(), CoreErrorCode::EmptyDocument);
assert_eq!(
ValidationError::EmptySection { section_index: 0 }.code(),
CoreErrorCode::EmptySection
);
assert_eq!(
ValidationError::EmptyParagraph { section_index: 0, paragraph_index: 0 }.code(),
CoreErrorCode::EmptyParagraph
);
}
#[test]
fn core_result_alias_works() {
fn ok_example() -> CoreResult<i32> {
Ok(42)
}
fn err_example() -> CoreResult<i32> {
Err(ValidationError::EmptyDocument)?
}
assert_eq!(ok_example().unwrap(), 42);
assert!(err_example().is_err());
}
#[test]
fn errors_are_send_and_sync() {
fn assert_send<T: Send>() {}
fn assert_sync<T: Sync>() {}
assert_send::<CoreError>();
assert_sync::<CoreError>();
assert_send::<ValidationError>();
assert_sync::<ValidationError>();
}
#[test]
fn core_error_implements_std_error() {
let err = CoreError::from(ValidationError::EmptyDocument);
let _: &dyn std::error::Error = &err;
}
#[test]
fn validation_error_eq() {
let a = ValidationError::EmptyDocument;
let b = ValidationError::EmptyDocument;
let c = ValidationError::EmptySection { section_index: 0 };
assert_eq!(a, b);
assert_ne!(a, c);
}
}