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 InvalidSummaryToken {
298 section_index: usize,
300 paragraph_index: usize,
302 run_index: usize,
304 token: String,
306 },
307}
308
309#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
325#[repr(u32)]
326pub enum CoreErrorCode {
327 EmptyDocument = 2000,
329 EmptySection = 2001,
331 EmptyParagraph = 2002,
333 EmptyTable = 2003,
335 EmptyTableRow = 2004,
337 InvalidSpan = 2005,
339 EmptyTextBox = 2006,
341 EmptyFootnote = 2007,
343 EmptyTableCell = 2008,
345 EmptyEndnote = 2009,
347 InvalidPolygon = 2010,
349 InvalidShapeDimension = 2011,
351 EmptyEquation = 2012,
353 EmptyChartData = 2013,
355 EmptyCategoryLabels = 2014,
357 MismatchedSeriesLengths = 2015,
359 NonLeadingTableHeaderRow = 2016,
361 InvalidSummaryToken = 2017,
363 InvalidGroupChild = 2020,
367 InvalidStructure = 2100,
369}
370
371impl std::fmt::Display for CoreErrorCode {
372 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
373 write!(f, "E{:04}", *self as u32)
374 }
375}
376
377impl ValidationError {
378 pub fn code(&self) -> CoreErrorCode {
380 match self {
381 Self::EmptyDocument => CoreErrorCode::EmptyDocument,
382 Self::EmptySection { .. } => CoreErrorCode::EmptySection,
383 Self::EmptyParagraph { .. } => CoreErrorCode::EmptyParagraph,
384 Self::EmptyTable { .. } => CoreErrorCode::EmptyTable,
385 Self::EmptyTableRow { .. } => CoreErrorCode::EmptyTableRow,
386 Self::InvalidSpan { .. } => CoreErrorCode::InvalidSpan,
387 Self::NonLeadingTableHeaderRow { .. } => CoreErrorCode::NonLeadingTableHeaderRow,
388 Self::EmptyTextBox { .. } => CoreErrorCode::EmptyTextBox,
389 Self::EmptyFootnote { .. } => CoreErrorCode::EmptyFootnote,
390 Self::InvalidGroupChild { .. } => CoreErrorCode::InvalidGroupChild,
391 Self::EmptyTableCell { .. } => CoreErrorCode::EmptyTableCell,
392 Self::EmptyEndnote { .. } => CoreErrorCode::EmptyEndnote,
393 Self::InvalidPolygon { .. } => CoreErrorCode::InvalidPolygon,
394 Self::InvalidShapeDimension { .. } => CoreErrorCode::InvalidShapeDimension,
395 Self::EmptyChartData { .. } => CoreErrorCode::EmptyChartData,
396 Self::EmptyCategoryLabels { .. } => CoreErrorCode::EmptyCategoryLabels,
397 Self::MismatchedSeriesLengths { .. } => CoreErrorCode::MismatchedSeriesLengths,
398 Self::EmptyEquation { .. } => CoreErrorCode::EmptyEquation,
399 Self::InvalidSummaryToken { .. } => CoreErrorCode::InvalidSummaryToken,
400 }
401 }
402}
403
404pub type CoreResult<T> = Result<T, CoreError>;
417
418#[cfg(test)]
419mod tests {
420 use super::*;
421
422 #[test]
425 fn empty_document_displays_message() {
426 let err = ValidationError::EmptyDocument;
427 let msg = err.to_string();
428 assert!(msg.contains("section"), "msg: {msg}");
429 assert!(msg.contains("at least 1"), "msg: {msg}");
430 }
431
432 #[test]
433 fn empty_section_displays_index() {
434 let err = ValidationError::EmptySection { section_index: 3 };
435 let msg = err.to_string();
436 assert!(msg.contains("3"), "msg: {msg}");
437 assert!(msg.contains("no paragraphs"), "msg: {msg}");
438 }
439
440 #[test]
441 fn empty_paragraph_displays_location() {
442 let err = ValidationError::EmptyParagraph { section_index: 1, paragraph_index: 5 };
443 let msg = err.to_string();
444 assert!(msg.contains("section 1"), "msg: {msg}");
445 assert!(msg.contains("paragraph 5"), "msg: {msg}");
446 }
447
448 #[test]
449 fn empty_table_displays_location() {
450 let err =
451 ValidationError::EmptyTable { section_index: 0, paragraph_index: 2, run_index: 0 };
452 let msg = err.to_string();
453 assert!(msg.contains("no rows"), "msg: {msg}");
454 }
455
456 #[test]
457 fn empty_table_row_displays_location() {
458 let err = ValidationError::EmptyTableRow {
459 section_index: 0,
460 paragraph_index: 0,
461 run_index: 0,
462 row_index: 1,
463 };
464 let msg = err.to_string();
465 assert!(msg.contains("row 1"), "msg: {msg}");
466 assert!(msg.contains("no cells"), "msg: {msg}");
467 }
468
469 #[test]
470 fn invalid_span_displays_all_context() {
471 let err = ValidationError::InvalidSpan {
472 field: "col_span",
473 value: 0,
474 section_index: 0,
475 paragraph_index: 1,
476 run_index: 0,
477 row_index: 0,
478 cell_index: 2,
479 };
480 let msg = err.to_string();
481 assert!(msg.contains("col_span"), "msg: {msg}");
482 assert!(msg.contains("= 0"), "msg: {msg}");
483 assert!(msg.contains("cell 2"), "msg: {msg}");
484 }
485
486 #[test]
487 fn empty_text_box_displays_location() {
488 let err =
489 ValidationError::EmptyTextBox { section_index: 0, paragraph_index: 0, run_index: 1 };
490 let msg = err.to_string();
491 assert!(msg.contains("TextBox"), "msg: {msg}");
492 }
493
494 #[test]
495 fn empty_footnote_displays_location() {
496 let err =
497 ValidationError::EmptyFootnote { section_index: 0, paragraph_index: 0, run_index: 0 };
498 let msg = err.to_string();
499 assert!(msg.contains("Footnote"), "msg: {msg}");
500 }
501
502 #[test]
503 fn empty_table_cell_displays_location() {
504 let err = ValidationError::EmptyTableCell {
505 section_index: 0,
506 paragraph_index: 0,
507 run_index: 0,
508 row_index: 0,
509 cell_index: 0,
510 };
511 let msg = err.to_string();
512 assert!(msg.contains("cell"), "msg: {msg}");
513 }
514
515 #[test]
518 fn core_error_from_validation() {
519 let ve = ValidationError::EmptyDocument;
520 let ce: CoreError = ve.into();
521 match ce {
522 CoreError::Validation(v) => assert_eq!(v, ValidationError::EmptyDocument),
523 other => panic!("expected Validation, got: {other}"),
524 }
525 }
526
527 #[test]
528 fn core_error_from_foundation() {
529 let fe =
530 FoundationError::InvalidField { field: "test".to_string(), reason: "bad".to_string() };
531 let ce: CoreError = fe.into();
532 assert!(matches!(ce, CoreError::Foundation(_)));
533 }
534
535 #[test]
536 fn core_error_invalid_structure() {
537 let ce = CoreError::InvalidStructure {
538 context: "document".to_string(),
539 reason: "circular reference".to_string(),
540 };
541 let msg = ce.to_string();
542 assert!(msg.contains("document"), "msg: {msg}");
543 assert!(msg.contains("circular"), "msg: {msg}");
544 }
545
546 #[test]
549 fn error_codes_in_core_range() {
550 assert_eq!(CoreErrorCode::EmptyDocument as u32, 2000);
551 assert_eq!(CoreErrorCode::EmptySection as u32, 2001);
552 assert_eq!(CoreErrorCode::EmptyParagraph as u32, 2002);
553 assert_eq!(CoreErrorCode::EmptyTable as u32, 2003);
554 assert_eq!(CoreErrorCode::EmptyTableRow as u32, 2004);
555 assert_eq!(CoreErrorCode::InvalidSpan as u32, 2005);
556 assert_eq!(CoreErrorCode::EmptyTextBox as u32, 2006);
557 assert_eq!(CoreErrorCode::EmptyFootnote as u32, 2007);
558 assert_eq!(CoreErrorCode::EmptyTableCell as u32, 2008);
559 assert_eq!(CoreErrorCode::InvalidStructure as u32, 2100);
560 }
561
562 #[test]
563 fn error_code_display_format() {
564 assert_eq!(CoreErrorCode::EmptyDocument.to_string(), "E2000");
565 assert_eq!(CoreErrorCode::InvalidStructure.to_string(), "E2100");
566 }
567
568 #[test]
569 fn validation_error_code_mapping() {
570 assert_eq!(ValidationError::EmptyDocument.code(), CoreErrorCode::EmptyDocument);
571 assert_eq!(
572 ValidationError::EmptySection { section_index: 0 }.code(),
573 CoreErrorCode::EmptySection
574 );
575 assert_eq!(
576 ValidationError::EmptyParagraph { section_index: 0, paragraph_index: 0 }.code(),
577 CoreErrorCode::EmptyParagraph
578 );
579 }
580
581 #[test]
584 fn core_result_alias_works() {
585 fn ok_example() -> CoreResult<i32> {
586 Ok(42)
587 }
588 fn err_example() -> CoreResult<i32> {
589 Err(ValidationError::EmptyDocument)?
590 }
591 assert_eq!(ok_example().unwrap(), 42);
592 assert!(err_example().is_err());
593 }
594
595 #[test]
598 fn errors_are_send_and_sync() {
599 fn assert_send<T: Send>() {}
600 fn assert_sync<T: Sync>() {}
601 assert_send::<CoreError>();
602 assert_sync::<CoreError>();
603 assert_send::<ValidationError>();
604 assert_sync::<ValidationError>();
605 }
606
607 #[test]
610 fn core_error_implements_std_error() {
611 let err = CoreError::from(ValidationError::EmptyDocument);
612 let _: &dyn std::error::Error = &err;
613 }
614
615 #[test]
618 fn validation_error_eq() {
619 let a = ValidationError::EmptyDocument;
620 let b = ValidationError::EmptyDocument;
621 let c = ValidationError::EmptySection { section_index: 0 };
622 assert_eq!(a, b);
623 assert_ne!(a, c);
624 }
625}