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
271#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
287#[repr(u32)]
288pub enum CoreErrorCode {
289 EmptyDocument = 2000,
291 EmptySection = 2001,
293 EmptyParagraph = 2002,
295 EmptyTable = 2003,
297 EmptyTableRow = 2004,
299 InvalidSpan = 2005,
301 EmptyTextBox = 2006,
303 EmptyFootnote = 2007,
305 EmptyTableCell = 2008,
307 EmptyEndnote = 2009,
309 InvalidPolygon = 2010,
311 InvalidShapeDimension = 2011,
313 EmptyEquation = 2012,
315 EmptyChartData = 2013,
317 EmptyCategoryLabels = 2014,
319 MismatchedSeriesLengths = 2015,
321 InvalidStructure = 2100,
323}
324
325impl std::fmt::Display for CoreErrorCode {
326 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
327 write!(f, "E{:04}", *self as u32)
328 }
329}
330
331impl ValidationError {
332 pub fn code(&self) -> CoreErrorCode {
334 match self {
335 Self::EmptyDocument => CoreErrorCode::EmptyDocument,
336 Self::EmptySection { .. } => CoreErrorCode::EmptySection,
337 Self::EmptyParagraph { .. } => CoreErrorCode::EmptyParagraph,
338 Self::EmptyTable { .. } => CoreErrorCode::EmptyTable,
339 Self::EmptyTableRow { .. } => CoreErrorCode::EmptyTableRow,
340 Self::InvalidSpan { .. } => CoreErrorCode::InvalidSpan,
341 Self::EmptyTextBox { .. } => CoreErrorCode::EmptyTextBox,
342 Self::EmptyFootnote { .. } => CoreErrorCode::EmptyFootnote,
343 Self::EmptyTableCell { .. } => CoreErrorCode::EmptyTableCell,
344 Self::EmptyEndnote { .. } => CoreErrorCode::EmptyEndnote,
345 Self::InvalidPolygon { .. } => CoreErrorCode::InvalidPolygon,
346 Self::InvalidShapeDimension { .. } => CoreErrorCode::InvalidShapeDimension,
347 Self::EmptyChartData { .. } => CoreErrorCode::EmptyChartData,
348 Self::EmptyCategoryLabels { .. } => CoreErrorCode::EmptyCategoryLabels,
349 Self::MismatchedSeriesLengths { .. } => CoreErrorCode::MismatchedSeriesLengths,
350 Self::EmptyEquation { .. } => CoreErrorCode::EmptyEquation,
351 }
352 }
353}
354
355pub type CoreResult<T> = Result<T, CoreError>;
368
369#[cfg(test)]
370mod tests {
371 use super::*;
372
373 #[test]
376 fn empty_document_displays_message() {
377 let err = ValidationError::EmptyDocument;
378 let msg = err.to_string();
379 assert!(msg.contains("section"), "msg: {msg}");
380 assert!(msg.contains("at least 1"), "msg: {msg}");
381 }
382
383 #[test]
384 fn empty_section_displays_index() {
385 let err = ValidationError::EmptySection { section_index: 3 };
386 let msg = err.to_string();
387 assert!(msg.contains("3"), "msg: {msg}");
388 assert!(msg.contains("no paragraphs"), "msg: {msg}");
389 }
390
391 #[test]
392 fn empty_paragraph_displays_location() {
393 let err = ValidationError::EmptyParagraph { section_index: 1, paragraph_index: 5 };
394 let msg = err.to_string();
395 assert!(msg.contains("section 1"), "msg: {msg}");
396 assert!(msg.contains("paragraph 5"), "msg: {msg}");
397 }
398
399 #[test]
400 fn empty_table_displays_location() {
401 let err =
402 ValidationError::EmptyTable { section_index: 0, paragraph_index: 2, run_index: 0 };
403 let msg = err.to_string();
404 assert!(msg.contains("no rows"), "msg: {msg}");
405 }
406
407 #[test]
408 fn empty_table_row_displays_location() {
409 let err = ValidationError::EmptyTableRow {
410 section_index: 0,
411 paragraph_index: 0,
412 run_index: 0,
413 row_index: 1,
414 };
415 let msg = err.to_string();
416 assert!(msg.contains("row 1"), "msg: {msg}");
417 assert!(msg.contains("no cells"), "msg: {msg}");
418 }
419
420 #[test]
421 fn invalid_span_displays_all_context() {
422 let err = ValidationError::InvalidSpan {
423 field: "col_span",
424 value: 0,
425 section_index: 0,
426 paragraph_index: 1,
427 run_index: 0,
428 row_index: 0,
429 cell_index: 2,
430 };
431 let msg = err.to_string();
432 assert!(msg.contains("col_span"), "msg: {msg}");
433 assert!(msg.contains("= 0"), "msg: {msg}");
434 assert!(msg.contains("cell 2"), "msg: {msg}");
435 }
436
437 #[test]
438 fn empty_text_box_displays_location() {
439 let err =
440 ValidationError::EmptyTextBox { section_index: 0, paragraph_index: 0, run_index: 1 };
441 let msg = err.to_string();
442 assert!(msg.contains("TextBox"), "msg: {msg}");
443 }
444
445 #[test]
446 fn empty_footnote_displays_location() {
447 let err =
448 ValidationError::EmptyFootnote { section_index: 0, paragraph_index: 0, run_index: 0 };
449 let msg = err.to_string();
450 assert!(msg.contains("Footnote"), "msg: {msg}");
451 }
452
453 #[test]
454 fn empty_table_cell_displays_location() {
455 let err = ValidationError::EmptyTableCell {
456 section_index: 0,
457 paragraph_index: 0,
458 run_index: 0,
459 row_index: 0,
460 cell_index: 0,
461 };
462 let msg = err.to_string();
463 assert!(msg.contains("cell"), "msg: {msg}");
464 }
465
466 #[test]
469 fn core_error_from_validation() {
470 let ve = ValidationError::EmptyDocument;
471 let ce: CoreError = ve.into();
472 match ce {
473 CoreError::Validation(v) => assert_eq!(v, ValidationError::EmptyDocument),
474 other => panic!("expected Validation, got: {other}"),
475 }
476 }
477
478 #[test]
479 fn core_error_from_foundation() {
480 let fe =
481 FoundationError::InvalidField { field: "test".to_string(), reason: "bad".to_string() };
482 let ce: CoreError = fe.into();
483 assert!(matches!(ce, CoreError::Foundation(_)));
484 }
485
486 #[test]
487 fn core_error_invalid_structure() {
488 let ce = CoreError::InvalidStructure {
489 context: "document".to_string(),
490 reason: "circular reference".to_string(),
491 };
492 let msg = ce.to_string();
493 assert!(msg.contains("document"), "msg: {msg}");
494 assert!(msg.contains("circular"), "msg: {msg}");
495 }
496
497 #[test]
500 fn error_codes_in_core_range() {
501 assert_eq!(CoreErrorCode::EmptyDocument as u32, 2000);
502 assert_eq!(CoreErrorCode::EmptySection as u32, 2001);
503 assert_eq!(CoreErrorCode::EmptyParagraph as u32, 2002);
504 assert_eq!(CoreErrorCode::EmptyTable as u32, 2003);
505 assert_eq!(CoreErrorCode::EmptyTableRow as u32, 2004);
506 assert_eq!(CoreErrorCode::InvalidSpan as u32, 2005);
507 assert_eq!(CoreErrorCode::EmptyTextBox as u32, 2006);
508 assert_eq!(CoreErrorCode::EmptyFootnote as u32, 2007);
509 assert_eq!(CoreErrorCode::EmptyTableCell as u32, 2008);
510 assert_eq!(CoreErrorCode::InvalidStructure as u32, 2100);
511 }
512
513 #[test]
514 fn error_code_display_format() {
515 assert_eq!(CoreErrorCode::EmptyDocument.to_string(), "E2000");
516 assert_eq!(CoreErrorCode::InvalidStructure.to_string(), "E2100");
517 }
518
519 #[test]
520 fn validation_error_code_mapping() {
521 assert_eq!(ValidationError::EmptyDocument.code(), CoreErrorCode::EmptyDocument);
522 assert_eq!(
523 ValidationError::EmptySection { section_index: 0 }.code(),
524 CoreErrorCode::EmptySection
525 );
526 assert_eq!(
527 ValidationError::EmptyParagraph { section_index: 0, paragraph_index: 0 }.code(),
528 CoreErrorCode::EmptyParagraph
529 );
530 }
531
532 #[test]
535 fn core_result_alias_works() {
536 fn ok_example() -> CoreResult<i32> {
537 Ok(42)
538 }
539 fn err_example() -> CoreResult<i32> {
540 Err(ValidationError::EmptyDocument)?
541 }
542 assert_eq!(ok_example().unwrap(), 42);
543 assert!(err_example().is_err());
544 }
545
546 #[test]
549 fn errors_are_send_and_sync() {
550 fn assert_send<T: Send>() {}
551 fn assert_sync<T: Sync>() {}
552 assert_send::<CoreError>();
553 assert_sync::<CoreError>();
554 assert_send::<ValidationError>();
555 assert_sync::<ValidationError>();
556 }
557
558 #[test]
561 fn core_error_implements_std_error() {
562 let err = CoreError::from(ValidationError::EmptyDocument);
563 let _: &dyn std::error::Error = &err;
564 }
565
566 #[test]
569 fn validation_error_eq() {
570 let a = ValidationError::EmptyDocument;
571 let b = ValidationError::EmptyDocument;
572 let c = ValidationError::EmptySection { section_index: 0 };
573 assert_eq!(a, b);
574 assert_ne!(a, c);
575 }
576}