1use hwpforge_foundation::FoundationError;
24
25#[derive(Debug, thiserror::Error)]
42#[non_exhaustive]
43pub enum CoreError {
44 #[error("Document validation failed: {0}")]
46 Validation(#[from] ValidationError),
47
48 #[error("Foundation error: {0}")]
50 Foundation(#[from] FoundationError),
51
52 #[error("Invalid document structure in {context}: {reason}")]
54 InvalidStructure {
55 context: String,
57 reason: String,
59 },
60}
61
62#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
79#[non_exhaustive]
80pub enum ValidationError {
81 #[error("Empty document: at least 1 section required")]
83 EmptyDocument,
84
85 #[error("Section {section_index} has no paragraphs")]
87 EmptySection {
88 section_index: usize,
90 },
91
92 #[error("Paragraph has no runs (section {section_index}, paragraph {paragraph_index})")]
94 EmptyParagraph {
95 section_index: usize,
97 paragraph_index: usize,
99 },
100
101 #[error(
103 "Table has no rows (section {section_index}, paragraph {paragraph_index}, run {run_index})"
104 )]
105 EmptyTable {
106 section_index: usize,
108 paragraph_index: usize,
110 run_index: usize,
112 },
113
114 #[error("Table row has no cells (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index})")]
116 EmptyTableRow {
117 section_index: usize,
119 paragraph_index: usize,
121 run_index: usize,
123 row_index: usize,
125 },
126
127 #[error("Invalid span: {field} = {value} (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index}, cell {cell_index})")]
129 InvalidSpan {
130 field: &'static str,
132 value: u16,
134 section_index: usize,
136 paragraph_index: usize,
138 run_index: usize,
140 row_index: usize,
142 cell_index: usize,
144 },
145
146 #[error("TextBox has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
148 EmptyTextBox {
149 section_index: usize,
151 paragraph_index: usize,
153 run_index: usize,
155 },
156
157 #[error("Group has a non-shape child (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
159 InvalidGroupChild {
160 section_index: usize,
162 paragraph_index: usize,
164 run_index: usize,
166 },
167
168 #[error("Footnote has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
170 EmptyFootnote {
171 section_index: usize,
173 paragraph_index: usize,
175 run_index: usize,
177 },
178
179 #[error("Endnote has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
181 EmptyEndnote {
182 section_index: usize,
184 paragraph_index: usize,
186 run_index: usize,
188 },
189
190 #[error("Polygon has invalid vertex count: {vertex_count} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
192 InvalidPolygon {
193 section_index: usize,
195 paragraph_index: usize,
197 run_index: usize,
199 vertex_count: usize,
201 },
202
203 #[error("Shape {shape_type} has zero dimension (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
205 InvalidShapeDimension {
206 section_index: usize,
208 paragraph_index: usize,
210 run_index: usize,
212 shape_type: &'static str,
214 },
215
216 #[error("Chart has empty data (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
218 EmptyChartData {
219 section_index: usize,
221 paragraph_index: usize,
223 run_index: usize,
225 },
226
227 #[error("Equation has empty script (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
229 EmptyEquation {
230 section_index: usize,
232 paragraph_index: usize,
234 run_index: usize,
236 },
237
238 #[error("Chart has empty category labels (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
240 EmptyCategoryLabels {
241 section_index: usize,
243 paragraph_index: usize,
245 run_index: usize,
247 },
248
249 #[error("XY series '{series_name}' has mismatched lengths: x={x_len}, y={y_len} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
251 MismatchedSeriesLengths {
252 section_index: usize,
254 paragraph_index: usize,
256 run_index: usize,
258 series_name: String,
260 x_len: usize,
262 y_len: usize,
264 },
265
266 #[error("Table cell has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index}, cell {cell_index})")]
268 EmptyTableCell {
269 section_index: usize,
271 paragraph_index: usize,
273 run_index: usize,
275 row_index: usize,
277 cell_index: usize,
279 },
280
281 #[error("Table header row is not part of the leading header block (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index})")]
283 NonLeadingTableHeaderRow {
284 section_index: usize,
286 paragraph_index: usize,
288 run_index: usize,
290 row_index: usize,
292 },
293
294 #[error("Invalid SUMMERY token {token:?} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
297 InvalidSummeryToken {
298 section_index: usize,
300 paragraph_index: usize,
302 run_index: usize,
304 token: String,
306 },
307
308 #[error("DateCodeField mismatch: is_time_mode={is_time_mode}, raw_command={raw_command:?} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
311 DateCodeFieldMismatch {
312 section_index: usize,
314 paragraph_index: usize,
316 run_index: usize,
318 is_time_mode: bool,
320 raw_command: String,
322 },
323
324 #[error("InlinePageNumber mismatch: kind {kind} expects raw_flag {expected:#X}, found {actual:#X} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
329 InlinePageNumberMismatch {
330 section_index: usize,
332 paragraph_index: usize,
334 run_index: usize,
336 kind: &'static str,
338 expected: u32,
340 actual: u32,
342 },
343}
344
345#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
361#[repr(u32)]
362pub enum CoreErrorCode {
363 EmptyDocument = 2000,
365 EmptySection = 2001,
367 EmptyParagraph = 2002,
369 EmptyTable = 2003,
371 EmptyTableRow = 2004,
373 InvalidSpan = 2005,
375 EmptyTextBox = 2006,
377 EmptyFootnote = 2007,
379 EmptyTableCell = 2008,
381 EmptyEndnote = 2009,
383 InvalidPolygon = 2010,
385 InvalidShapeDimension = 2011,
387 EmptyEquation = 2012,
389 EmptyChartData = 2013,
391 EmptyCategoryLabels = 2014,
393 MismatchedSeriesLengths = 2015,
395 NonLeadingTableHeaderRow = 2016,
397 InvalidSummeryToken = 2017,
399 DateCodeFieldMismatch = 2018,
401 InlinePageNumberMismatch = 2019,
403 InvalidGroupChild = 2020,
405 InvalidStructure = 2100,
407}
408
409impl std::fmt::Display for CoreErrorCode {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 write!(f, "E{:04}", *self as u32)
412 }
413}
414
415impl ValidationError {
416 pub fn code(&self) -> CoreErrorCode {
418 match self {
419 Self::EmptyDocument => CoreErrorCode::EmptyDocument,
420 Self::EmptySection { .. } => CoreErrorCode::EmptySection,
421 Self::EmptyParagraph { .. } => CoreErrorCode::EmptyParagraph,
422 Self::EmptyTable { .. } => CoreErrorCode::EmptyTable,
423 Self::EmptyTableRow { .. } => CoreErrorCode::EmptyTableRow,
424 Self::InvalidSpan { .. } => CoreErrorCode::InvalidSpan,
425 Self::NonLeadingTableHeaderRow { .. } => CoreErrorCode::NonLeadingTableHeaderRow,
426 Self::EmptyTextBox { .. } => CoreErrorCode::EmptyTextBox,
427 Self::EmptyFootnote { .. } => CoreErrorCode::EmptyFootnote,
428 Self::InvalidGroupChild { .. } => CoreErrorCode::InvalidGroupChild,
429 Self::EmptyTableCell { .. } => CoreErrorCode::EmptyTableCell,
430 Self::EmptyEndnote { .. } => CoreErrorCode::EmptyEndnote,
431 Self::InvalidPolygon { .. } => CoreErrorCode::InvalidPolygon,
432 Self::InvalidShapeDimension { .. } => CoreErrorCode::InvalidShapeDimension,
433 Self::EmptyChartData { .. } => CoreErrorCode::EmptyChartData,
434 Self::EmptyCategoryLabels { .. } => CoreErrorCode::EmptyCategoryLabels,
435 Self::MismatchedSeriesLengths { .. } => CoreErrorCode::MismatchedSeriesLengths,
436 Self::EmptyEquation { .. } => CoreErrorCode::EmptyEquation,
437 Self::InvalidSummeryToken { .. } => CoreErrorCode::InvalidSummeryToken,
438 Self::DateCodeFieldMismatch { .. } => CoreErrorCode::DateCodeFieldMismatch,
439 Self::InlinePageNumberMismatch { .. } => CoreErrorCode::InlinePageNumberMismatch,
440 }
441 }
442}
443
444pub type CoreResult<T> = Result<T, CoreError>;
457
458#[cfg(test)]
459mod tests {
460 use super::*;
461
462 #[test]
465 fn empty_document_displays_message() {
466 let err = ValidationError::EmptyDocument;
467 let msg = err.to_string();
468 assert!(msg.contains("section"), "msg: {msg}");
469 assert!(msg.contains("at least 1"), "msg: {msg}");
470 }
471
472 #[test]
473 fn empty_section_displays_index() {
474 let err = ValidationError::EmptySection { section_index: 3 };
475 let msg = err.to_string();
476 assert!(msg.contains("3"), "msg: {msg}");
477 assert!(msg.contains("no paragraphs"), "msg: {msg}");
478 }
479
480 #[test]
481 fn empty_paragraph_displays_location() {
482 let err = ValidationError::EmptyParagraph { section_index: 1, paragraph_index: 5 };
483 let msg = err.to_string();
484 assert!(msg.contains("section 1"), "msg: {msg}");
485 assert!(msg.contains("paragraph 5"), "msg: {msg}");
486 }
487
488 #[test]
489 fn empty_table_displays_location() {
490 let err =
491 ValidationError::EmptyTable { section_index: 0, paragraph_index: 2, run_index: 0 };
492 let msg = err.to_string();
493 assert!(msg.contains("no rows"), "msg: {msg}");
494 }
495
496 #[test]
497 fn empty_table_row_displays_location() {
498 let err = ValidationError::EmptyTableRow {
499 section_index: 0,
500 paragraph_index: 0,
501 run_index: 0,
502 row_index: 1,
503 };
504 let msg = err.to_string();
505 assert!(msg.contains("row 1"), "msg: {msg}");
506 assert!(msg.contains("no cells"), "msg: {msg}");
507 }
508
509 #[test]
510 fn invalid_span_displays_all_context() {
511 let err = ValidationError::InvalidSpan {
512 field: "col_span",
513 value: 0,
514 section_index: 0,
515 paragraph_index: 1,
516 run_index: 0,
517 row_index: 0,
518 cell_index: 2,
519 };
520 let msg = err.to_string();
521 assert!(msg.contains("col_span"), "msg: {msg}");
522 assert!(msg.contains("= 0"), "msg: {msg}");
523 assert!(msg.contains("cell 2"), "msg: {msg}");
524 }
525
526 #[test]
527 fn empty_text_box_displays_location() {
528 let err =
529 ValidationError::EmptyTextBox { section_index: 0, paragraph_index: 0, run_index: 1 };
530 let msg = err.to_string();
531 assert!(msg.contains("TextBox"), "msg: {msg}");
532 }
533
534 #[test]
535 fn empty_footnote_displays_location() {
536 let err =
537 ValidationError::EmptyFootnote { section_index: 0, paragraph_index: 0, run_index: 0 };
538 let msg = err.to_string();
539 assert!(msg.contains("Footnote"), "msg: {msg}");
540 }
541
542 #[test]
543 fn empty_table_cell_displays_location() {
544 let err = ValidationError::EmptyTableCell {
545 section_index: 0,
546 paragraph_index: 0,
547 run_index: 0,
548 row_index: 0,
549 cell_index: 0,
550 };
551 let msg = err.to_string();
552 assert!(msg.contains("cell"), "msg: {msg}");
553 }
554
555 #[test]
558 fn core_error_from_validation() {
559 let ve = ValidationError::EmptyDocument;
560 let ce: CoreError = ve.into();
561 match ce {
562 CoreError::Validation(v) => assert_eq!(v, ValidationError::EmptyDocument),
563 other => panic!("expected Validation, got: {other}"),
564 }
565 }
566
567 #[test]
568 fn core_error_from_foundation() {
569 let fe =
570 FoundationError::InvalidField { field: "test".to_string(), reason: "bad".to_string() };
571 let ce: CoreError = fe.into();
572 assert!(matches!(ce, CoreError::Foundation(_)));
573 }
574
575 #[test]
576 fn core_error_invalid_structure() {
577 let ce = CoreError::InvalidStructure {
578 context: "document".to_string(),
579 reason: "circular reference".to_string(),
580 };
581 let msg = ce.to_string();
582 assert!(msg.contains("document"), "msg: {msg}");
583 assert!(msg.contains("circular"), "msg: {msg}");
584 }
585
586 #[test]
589 fn error_codes_in_core_range() {
590 assert_eq!(CoreErrorCode::EmptyDocument as u32, 2000);
591 assert_eq!(CoreErrorCode::EmptySection as u32, 2001);
592 assert_eq!(CoreErrorCode::EmptyParagraph as u32, 2002);
593 assert_eq!(CoreErrorCode::EmptyTable as u32, 2003);
594 assert_eq!(CoreErrorCode::EmptyTableRow as u32, 2004);
595 assert_eq!(CoreErrorCode::InvalidSpan as u32, 2005);
596 assert_eq!(CoreErrorCode::EmptyTextBox as u32, 2006);
597 assert_eq!(CoreErrorCode::EmptyFootnote as u32, 2007);
598 assert_eq!(CoreErrorCode::EmptyTableCell as u32, 2008);
599 assert_eq!(CoreErrorCode::InvalidStructure as u32, 2100);
600 }
601
602 #[test]
603 fn error_code_display_format() {
604 assert_eq!(CoreErrorCode::EmptyDocument.to_string(), "E2000");
605 assert_eq!(CoreErrorCode::InvalidStructure.to_string(), "E2100");
606 }
607
608 #[test]
609 fn validation_error_code_mapping() {
610 assert_eq!(ValidationError::EmptyDocument.code(), CoreErrorCode::EmptyDocument);
611 assert_eq!(
612 ValidationError::EmptySection { section_index: 0 }.code(),
613 CoreErrorCode::EmptySection
614 );
615 assert_eq!(
616 ValidationError::EmptyParagraph { section_index: 0, paragraph_index: 0 }.code(),
617 CoreErrorCode::EmptyParagraph
618 );
619 }
620
621 #[test]
624 fn core_result_alias_works() {
625 fn ok_example() -> CoreResult<i32> {
626 Ok(42)
627 }
628 fn err_example() -> CoreResult<i32> {
629 Err(ValidationError::EmptyDocument)?
630 }
631 assert_eq!(ok_example().unwrap(), 42);
632 assert!(err_example().is_err());
633 }
634
635 #[test]
638 fn errors_are_send_and_sync() {
639 fn assert_send<T: Send>() {}
640 fn assert_sync<T: Sync>() {}
641 assert_send::<CoreError>();
642 assert_sync::<CoreError>();
643 assert_send::<ValidationError>();
644 assert_sync::<ValidationError>();
645 }
646
647 #[test]
650 fn core_error_implements_std_error() {
651 let err = CoreError::from(ValidationError::EmptyDocument);
652 let _: &dyn std::error::Error = &err;
653 }
654
655 #[test]
658 fn validation_error_eq() {
659 let a = ValidationError::EmptyDocument;
660 let b = ValidationError::EmptyDocument;
661 let c = ValidationError::EmptySection { section_index: 0 };
662 assert_eq!(a, b);
663 assert_ne!(a, c);
664 }
665}