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("Footnote has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
159 EmptyFootnote {
160 section_index: usize,
162 paragraph_index: usize,
164 run_index: usize,
166 },
167
168 #[error("Endnote has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
170 EmptyEndnote {
171 section_index: usize,
173 paragraph_index: usize,
175 run_index: usize,
177 },
178
179 #[error("Polygon has invalid vertex count: {vertex_count} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
181 InvalidPolygon {
182 section_index: usize,
184 paragraph_index: usize,
186 run_index: usize,
188 vertex_count: usize,
190 },
191
192 #[error("Shape {shape_type} has zero dimension (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
194 InvalidShapeDimension {
195 section_index: usize,
197 paragraph_index: usize,
199 run_index: usize,
201 shape_type: &'static str,
203 },
204
205 #[error("Chart has empty data (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
207 EmptyChartData {
208 section_index: usize,
210 paragraph_index: usize,
212 run_index: usize,
214 },
215
216 #[error("Equation has empty script (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
218 EmptyEquation {
219 section_index: usize,
221 paragraph_index: usize,
223 run_index: usize,
225 },
226
227 #[error("Chart has empty category labels (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
229 EmptyCategoryLabels {
230 section_index: usize,
232 paragraph_index: usize,
234 run_index: usize,
236 },
237
238 #[error("XY series '{series_name}' has mismatched lengths: x={x_len}, y={y_len} (section {section_index}, paragraph {paragraph_index}, run {run_index})")]
240 MismatchedSeriesLengths {
241 section_index: usize,
243 paragraph_index: usize,
245 run_index: usize,
247 series_name: String,
249 x_len: usize,
251 y_len: usize,
253 },
254
255 #[error("Table cell has no paragraphs (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index}, cell {cell_index})")]
257 EmptyTableCell {
258 section_index: usize,
260 paragraph_index: usize,
262 run_index: usize,
264 row_index: usize,
266 cell_index: usize,
268 },
269
270 #[error("Table header row is not part of the leading header block (section {section_index}, paragraph {paragraph_index}, run {run_index}, row {row_index})")]
272 NonLeadingTableHeaderRow {
273 section_index: usize,
275 paragraph_index: usize,
277 run_index: usize,
279 row_index: usize,
281 },
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
300#[repr(u32)]
301pub enum CoreErrorCode {
302 EmptyDocument = 2000,
304 EmptySection = 2001,
306 EmptyParagraph = 2002,
308 EmptyTable = 2003,
310 EmptyTableRow = 2004,
312 InvalidSpan = 2005,
314 EmptyTextBox = 2006,
316 EmptyFootnote = 2007,
318 EmptyTableCell = 2008,
320 EmptyEndnote = 2009,
322 InvalidPolygon = 2010,
324 InvalidShapeDimension = 2011,
326 EmptyEquation = 2012,
328 EmptyChartData = 2013,
330 EmptyCategoryLabels = 2014,
332 MismatchedSeriesLengths = 2015,
334 NonLeadingTableHeaderRow = 2016,
336 InvalidStructure = 2100,
338}
339
340impl std::fmt::Display for CoreErrorCode {
341 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
342 write!(f, "E{:04}", *self as u32)
343 }
344}
345
346impl ValidationError {
347 pub fn code(&self) -> CoreErrorCode {
349 match self {
350 Self::EmptyDocument => CoreErrorCode::EmptyDocument,
351 Self::EmptySection { .. } => CoreErrorCode::EmptySection,
352 Self::EmptyParagraph { .. } => CoreErrorCode::EmptyParagraph,
353 Self::EmptyTable { .. } => CoreErrorCode::EmptyTable,
354 Self::EmptyTableRow { .. } => CoreErrorCode::EmptyTableRow,
355 Self::InvalidSpan { .. } => CoreErrorCode::InvalidSpan,
356 Self::NonLeadingTableHeaderRow { .. } => CoreErrorCode::NonLeadingTableHeaderRow,
357 Self::EmptyTextBox { .. } => CoreErrorCode::EmptyTextBox,
358 Self::EmptyFootnote { .. } => CoreErrorCode::EmptyFootnote,
359 Self::EmptyTableCell { .. } => CoreErrorCode::EmptyTableCell,
360 Self::EmptyEndnote { .. } => CoreErrorCode::EmptyEndnote,
361 Self::InvalidPolygon { .. } => CoreErrorCode::InvalidPolygon,
362 Self::InvalidShapeDimension { .. } => CoreErrorCode::InvalidShapeDimension,
363 Self::EmptyChartData { .. } => CoreErrorCode::EmptyChartData,
364 Self::EmptyCategoryLabels { .. } => CoreErrorCode::EmptyCategoryLabels,
365 Self::MismatchedSeriesLengths { .. } => CoreErrorCode::MismatchedSeriesLengths,
366 Self::EmptyEquation { .. } => CoreErrorCode::EmptyEquation,
367 }
368 }
369}
370
371pub type CoreResult<T> = Result<T, CoreError>;
384
385#[cfg(test)]
386mod tests {
387 use super::*;
388
389 #[test]
392 fn empty_document_displays_message() {
393 let err = ValidationError::EmptyDocument;
394 let msg = err.to_string();
395 assert!(msg.contains("section"), "msg: {msg}");
396 assert!(msg.contains("at least 1"), "msg: {msg}");
397 }
398
399 #[test]
400 fn empty_section_displays_index() {
401 let err = ValidationError::EmptySection { section_index: 3 };
402 let msg = err.to_string();
403 assert!(msg.contains("3"), "msg: {msg}");
404 assert!(msg.contains("no paragraphs"), "msg: {msg}");
405 }
406
407 #[test]
408 fn empty_paragraph_displays_location() {
409 let err = ValidationError::EmptyParagraph { section_index: 1, paragraph_index: 5 };
410 let msg = err.to_string();
411 assert!(msg.contains("section 1"), "msg: {msg}");
412 assert!(msg.contains("paragraph 5"), "msg: {msg}");
413 }
414
415 #[test]
416 fn empty_table_displays_location() {
417 let err =
418 ValidationError::EmptyTable { section_index: 0, paragraph_index: 2, run_index: 0 };
419 let msg = err.to_string();
420 assert!(msg.contains("no rows"), "msg: {msg}");
421 }
422
423 #[test]
424 fn empty_table_row_displays_location() {
425 let err = ValidationError::EmptyTableRow {
426 section_index: 0,
427 paragraph_index: 0,
428 run_index: 0,
429 row_index: 1,
430 };
431 let msg = err.to_string();
432 assert!(msg.contains("row 1"), "msg: {msg}");
433 assert!(msg.contains("no cells"), "msg: {msg}");
434 }
435
436 #[test]
437 fn invalid_span_displays_all_context() {
438 let err = ValidationError::InvalidSpan {
439 field: "col_span",
440 value: 0,
441 section_index: 0,
442 paragraph_index: 1,
443 run_index: 0,
444 row_index: 0,
445 cell_index: 2,
446 };
447 let msg = err.to_string();
448 assert!(msg.contains("col_span"), "msg: {msg}");
449 assert!(msg.contains("= 0"), "msg: {msg}");
450 assert!(msg.contains("cell 2"), "msg: {msg}");
451 }
452
453 #[test]
454 fn empty_text_box_displays_location() {
455 let err =
456 ValidationError::EmptyTextBox { section_index: 0, paragraph_index: 0, run_index: 1 };
457 let msg = err.to_string();
458 assert!(msg.contains("TextBox"), "msg: {msg}");
459 }
460
461 #[test]
462 fn empty_footnote_displays_location() {
463 let err =
464 ValidationError::EmptyFootnote { section_index: 0, paragraph_index: 0, run_index: 0 };
465 let msg = err.to_string();
466 assert!(msg.contains("Footnote"), "msg: {msg}");
467 }
468
469 #[test]
470 fn empty_table_cell_displays_location() {
471 let err = ValidationError::EmptyTableCell {
472 section_index: 0,
473 paragraph_index: 0,
474 run_index: 0,
475 row_index: 0,
476 cell_index: 0,
477 };
478 let msg = err.to_string();
479 assert!(msg.contains("cell"), "msg: {msg}");
480 }
481
482 #[test]
485 fn core_error_from_validation() {
486 let ve = ValidationError::EmptyDocument;
487 let ce: CoreError = ve.into();
488 match ce {
489 CoreError::Validation(v) => assert_eq!(v, ValidationError::EmptyDocument),
490 other => panic!("expected Validation, got: {other}"),
491 }
492 }
493
494 #[test]
495 fn core_error_from_foundation() {
496 let fe =
497 FoundationError::InvalidField { field: "test".to_string(), reason: "bad".to_string() };
498 let ce: CoreError = fe.into();
499 assert!(matches!(ce, CoreError::Foundation(_)));
500 }
501
502 #[test]
503 fn core_error_invalid_structure() {
504 let ce = CoreError::InvalidStructure {
505 context: "document".to_string(),
506 reason: "circular reference".to_string(),
507 };
508 let msg = ce.to_string();
509 assert!(msg.contains("document"), "msg: {msg}");
510 assert!(msg.contains("circular"), "msg: {msg}");
511 }
512
513 #[test]
516 fn error_codes_in_core_range() {
517 assert_eq!(CoreErrorCode::EmptyDocument as u32, 2000);
518 assert_eq!(CoreErrorCode::EmptySection as u32, 2001);
519 assert_eq!(CoreErrorCode::EmptyParagraph as u32, 2002);
520 assert_eq!(CoreErrorCode::EmptyTable as u32, 2003);
521 assert_eq!(CoreErrorCode::EmptyTableRow as u32, 2004);
522 assert_eq!(CoreErrorCode::InvalidSpan as u32, 2005);
523 assert_eq!(CoreErrorCode::EmptyTextBox as u32, 2006);
524 assert_eq!(CoreErrorCode::EmptyFootnote as u32, 2007);
525 assert_eq!(CoreErrorCode::EmptyTableCell as u32, 2008);
526 assert_eq!(CoreErrorCode::InvalidStructure as u32, 2100);
527 }
528
529 #[test]
530 fn error_code_display_format() {
531 assert_eq!(CoreErrorCode::EmptyDocument.to_string(), "E2000");
532 assert_eq!(CoreErrorCode::InvalidStructure.to_string(), "E2100");
533 }
534
535 #[test]
536 fn validation_error_code_mapping() {
537 assert_eq!(ValidationError::EmptyDocument.code(), CoreErrorCode::EmptyDocument);
538 assert_eq!(
539 ValidationError::EmptySection { section_index: 0 }.code(),
540 CoreErrorCode::EmptySection
541 );
542 assert_eq!(
543 ValidationError::EmptyParagraph { section_index: 0, paragraph_index: 0 }.code(),
544 CoreErrorCode::EmptyParagraph
545 );
546 }
547
548 #[test]
551 fn core_result_alias_works() {
552 fn ok_example() -> CoreResult<i32> {
553 Ok(42)
554 }
555 fn err_example() -> CoreResult<i32> {
556 Err(ValidationError::EmptyDocument)?
557 }
558 assert_eq!(ok_example().unwrap(), 42);
559 assert!(err_example().is_err());
560 }
561
562 #[test]
565 fn errors_are_send_and_sync() {
566 fn assert_send<T: Send>() {}
567 fn assert_sync<T: Sync>() {}
568 assert_send::<CoreError>();
569 assert_sync::<CoreError>();
570 assert_send::<ValidationError>();
571 assert_sync::<ValidationError>();
572 }
573
574 #[test]
577 fn core_error_implements_std_error() {
578 let err = CoreError::from(ValidationError::EmptyDocument);
579 let _: &dyn std::error::Error = &err;
580 }
581
582 #[test]
585 fn validation_error_eq() {
586 let a = ValidationError::EmptyDocument;
587 let b = ValidationError::EmptyDocument;
588 let c = ValidationError::EmptySection { section_index: 0 };
589 assert_eq!(a, b);
590 assert_ne!(a, c);
591 }
592}