1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2use serde::{Deserialize, Serialize};
9use std::fmt;
10use thiserror::Error;
11
12pub type Result<T> = std::result::Result<T, Error>;
14
15#[derive(Error, Debug, Clone, PartialEq)]
31pub struct Error {
32 pub code: ErrorCode,
34
35 pub message: String,
37
38 pub context: Option<ErrorContext>,
40}
41
42impl fmt::Display for Error {
44 #[inline]
45 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
46 write!(f, "{}: {}", self.code, self.message)?;
47 if let Some(ref ctx) = self.context {
48 write!(f, " ({ctx})")?;
49 }
50 Ok(())
51 }
52}
53
54impl Error {
55 #[inline]
57 #[must_use]
58 pub const fn code(&self) -> ErrorCode {
59 self.code
60 }
61
62 #[inline]
64 #[must_use]
65 pub const fn family_prefix(&self) -> &'static str {
66 self.code.family_prefix()
67 }
68}
69
70#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
102#[allow(non_camel_case_types)] pub enum ErrorCode {
104 CBKP001_SYNTAX,
109 CBKP011_UNSUPPORTED_CLAUSE,
111 CBKP021_ODO_NOT_TAIL,
113 CBKP022_NESTED_ODO,
115 CBKP023_ODO_REDEFINES,
117 CBKP051_UNSUPPORTED_EDITED_PIC,
119 CBKP101_INVALID_PIC,
121
122 CBKS121_COUNTER_NOT_FOUND,
127 CBKS141_RECORD_TOO_LARGE,
129 CBKS301_ODO_CLIPPED,
131 CBKS302_ODO_RAISED,
133 CBKS601_RENAME_UNKNOWN_FROM,
135 CBKS602_RENAME_UNKNOWN_THRU,
137 CBKS603_RENAME_NOT_CONTIGUOUS,
139 CBKS604_RENAME_REVERSED_RANGE,
141 CBKS605_RENAME_FROM_CROSSES_GROUP,
143 CBKS606_RENAME_THRU_CROSSES_GROUP,
145 CBKS607_RENAME_CROSSES_OCCURS,
147 CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND,
149 CBKS609_RENAME_OVER_REDEFINES,
151 CBKS610_RENAME_MULTIPLE_REDEFINES,
153 CBKS611_RENAME_PARTIAL_OCCURS,
155 CBKS612_RENAME_ODO_NOT_SUPPORTED,
157 CBKS701_PROJECTION_INVALID_ODO,
159 CBKS702_PROJECTION_UNRESOLVED_ALIAS,
161 CBKS703_PROJECTION_FIELD_NOT_FOUND,
163
164 CBKR101_FIXED_RECORD_ERROR,
169 CBKR201_RDW_READ_ERROR,
171 CBKR211_RDW_RESERVED_NONZERO,
173
174 CBKC201_JSON_WRITE_ERROR,
179 CBKC301_INVALID_EBCDIC_BYTE,
181
182 CBKD101_INVALID_FIELD_TYPE,
187 CBKD301_RECORD_TOO_SHORT,
189 CBKD302_EDITED_PIC_NOT_IMPLEMENTED,
191 CBKD401_COMP3_INVALID_NIBBLE,
193 CBKD410_ZONED_OVERFLOW,
195 CBKD411_ZONED_BAD_SIGN,
197 CBKD412_ZONED_BLANK_IS_ZERO,
199 CBKD413_ZONED_INVALID_ENCODING,
201 CBKD414_ZONED_MIXED_ENCODING,
203 CBKD415_ZONED_ENCODING_AMBIGUOUS,
205 CBKD421_EDITED_PIC_INVALID_FORMAT,
207 CBKD422_EDITED_PIC_SIGN_MISMATCH,
209 CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
211 CBKD431_FLOAT_NAN,
215 CBKD432_FLOAT_INFINITY,
219
220 CBKI001_INVALID_STATE,
225
226 CBKE501_JSON_TYPE_MISMATCH,
231 CBKE505_SCALE_MISMATCH,
233 CBKE510_NUMERIC_OVERFLOW,
235 CBKE515_STRING_LENGTH_VIOLATION,
237 CBKE521_ARRAY_LEN_OOB,
239 CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
241 CBKE531_FLOAT_ENCODE_OVERFLOW,
243
244 CBKF102_RECORD_LENGTH_INVALID,
249 CBKF104_RDW_SUSPECT_ASCII,
251 CBKF221_RDW_UNDERFLOW,
253
254 CBKA001_BASELINE_ERROR,
259
260 CBKW001_SCHEMA_CONVERSION,
265 CBKW002_TYPE_MAPPING,
267 CBKW003_DECIMAL_OVERFLOW,
269 CBKW004_BATCH_BUILD,
271 CBKW005_PARQUET_WRITE,
273}
274
275impl fmt::Display for ErrorCode {
276 #[inline]
277 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
278 let code_str = match self {
279 ErrorCode::CBKP001_SYNTAX => "CBKP001_SYNTAX",
280 ErrorCode::CBKP011_UNSUPPORTED_CLAUSE => "CBKP011_UNSUPPORTED_CLAUSE",
281 ErrorCode::CBKP021_ODO_NOT_TAIL => "CBKP021_ODO_NOT_TAIL",
282 ErrorCode::CBKP022_NESTED_ODO => "CBKP022_NESTED_ODO",
283 ErrorCode::CBKP023_ODO_REDEFINES => "CBKP023_ODO_REDEFINES",
284 ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC => "CBKP051_UNSUPPORTED_EDITED_PIC",
285 ErrorCode::CBKP101_INVALID_PIC => "CBKP101_INVALID_PIC",
286 ErrorCode::CBKS121_COUNTER_NOT_FOUND => "CBKS121_COUNTER_NOT_FOUND",
287 ErrorCode::CBKS141_RECORD_TOO_LARGE => "CBKS141_RECORD_TOO_LARGE",
288 ErrorCode::CBKS301_ODO_CLIPPED => "CBKS301_ODO_CLIPPED",
289 ErrorCode::CBKS302_ODO_RAISED => "CBKS302_ODO_RAISED",
290 ErrorCode::CBKS601_RENAME_UNKNOWN_FROM => "CBKS601_RENAME_UNKNOWN_FROM",
291 ErrorCode::CBKS602_RENAME_UNKNOWN_THRU => "CBKS602_RENAME_UNKNOWN_THRU",
292 ErrorCode::CBKS603_RENAME_NOT_CONTIGUOUS => "CBKS603_RENAME_NOT_CONTIGUOUS",
293 ErrorCode::CBKS604_RENAME_REVERSED_RANGE => "CBKS604_RENAME_REVERSED_RANGE",
294 ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP => "CBKS605_RENAME_FROM_CROSSES_GROUP",
295 ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP => "CBKS606_RENAME_THRU_CROSSES_GROUP",
296 ErrorCode::CBKS607_RENAME_CROSSES_OCCURS => "CBKS607_RENAME_CROSSES_OCCURS",
297 ErrorCode::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND => {
298 "CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND"
299 }
300 ErrorCode::CBKS609_RENAME_OVER_REDEFINES => "CBKS609_RENAME_OVER_REDEFINES",
301 ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES => "CBKS610_RENAME_MULTIPLE_REDEFINES",
302 ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS => "CBKS611_RENAME_PARTIAL_OCCURS",
303 ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED => "CBKS612_RENAME_ODO_NOT_SUPPORTED",
304 ErrorCode::CBKS701_PROJECTION_INVALID_ODO => "CBKS701_PROJECTION_INVALID_ODO",
305 ErrorCode::CBKS702_PROJECTION_UNRESOLVED_ALIAS => "CBKS702_PROJECTION_UNRESOLVED_ALIAS",
306 ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND => "CBKS703_PROJECTION_FIELD_NOT_FOUND",
307 ErrorCode::CBKR101_FIXED_RECORD_ERROR => "CBKR101_FIXED_RECORD_ERROR",
308 ErrorCode::CBKR201_RDW_READ_ERROR => "CBKR201_RDW_READ_ERROR",
309 ErrorCode::CBKR211_RDW_RESERVED_NONZERO => "CBKR211_RDW_RESERVED_NONZERO",
310 ErrorCode::CBKC201_JSON_WRITE_ERROR => "CBKC201_JSON_WRITE_ERROR",
311 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE => "CBKC301_INVALID_EBCDIC_BYTE",
312 ErrorCode::CBKD101_INVALID_FIELD_TYPE => "CBKD101_INVALID_FIELD_TYPE",
313 ErrorCode::CBKD301_RECORD_TOO_SHORT => "CBKD301_RECORD_TOO_SHORT",
314 ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED => "CBKD302_EDITED_PIC_NOT_IMPLEMENTED",
315 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE => "CBKD401_COMP3_INVALID_NIBBLE",
316 ErrorCode::CBKD410_ZONED_OVERFLOW => "CBKD410_ZONED_OVERFLOW",
317 ErrorCode::CBKD411_ZONED_BAD_SIGN => "CBKD411_ZONED_BAD_SIGN",
318 ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO => "CBKD412_ZONED_BLANK_IS_ZERO",
319 ErrorCode::CBKD413_ZONED_INVALID_ENCODING => "CBKD413_ZONED_INVALID_ENCODING",
320 ErrorCode::CBKD414_ZONED_MIXED_ENCODING => "CBKD414_ZONED_MIXED_ENCODING",
321 ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS => "CBKD415_ZONED_ENCODING_AMBIGUOUS",
322 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT => "CBKD421_EDITED_PIC_INVALID_FORMAT",
323 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH => "CBKD422_EDITED_PIC_SIGN_MISMATCH",
324 ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO => "CBKD423_EDITED_PIC_BLANK_WHEN_ZERO",
325 ErrorCode::CBKD431_FLOAT_NAN => "CBKD431_FLOAT_NAN",
326 ErrorCode::CBKD432_FLOAT_INFINITY => "CBKD432_FLOAT_INFINITY",
327 ErrorCode::CBKI001_INVALID_STATE => "CBKI001_INVALID_STATE",
328 ErrorCode::CBKE501_JSON_TYPE_MISMATCH => "CBKE501_JSON_TYPE_MISMATCH",
329 ErrorCode::CBKE505_SCALE_MISMATCH => "CBKE505_SCALE_MISMATCH",
330 ErrorCode::CBKE510_NUMERIC_OVERFLOW => "CBKE510_NUMERIC_OVERFLOW",
331 ErrorCode::CBKE515_STRING_LENGTH_VIOLATION => "CBKE515_STRING_LENGTH_VIOLATION",
332 ErrorCode::CBKE521_ARRAY_LEN_OOB => "CBKE521_ARRAY_LEN_OOB",
333 ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR => "CBKE530_SIGN_SEPARATE_ENCODE_ERROR",
334 ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW => "CBKE531_FLOAT_ENCODE_OVERFLOW",
335 ErrorCode::CBKF102_RECORD_LENGTH_INVALID => "CBKF102_RECORD_LENGTH_INVALID",
336 ErrorCode::CBKF104_RDW_SUSPECT_ASCII => "CBKF104_RDW_SUSPECT_ASCII",
337 ErrorCode::CBKF221_RDW_UNDERFLOW => "CBKF221_RDW_UNDERFLOW",
338 ErrorCode::CBKA001_BASELINE_ERROR => "CBKA001_BASELINE_ERROR",
339 ErrorCode::CBKW001_SCHEMA_CONVERSION => "CBKW001_SCHEMA_CONVERSION",
340 ErrorCode::CBKW002_TYPE_MAPPING => "CBKW002_TYPE_MAPPING",
341 ErrorCode::CBKW003_DECIMAL_OVERFLOW => "CBKW003_DECIMAL_OVERFLOW",
342 ErrorCode::CBKW004_BATCH_BUILD => "CBKW004_BATCH_BUILD",
343 ErrorCode::CBKW005_PARQUET_WRITE => "CBKW005_PARQUET_WRITE",
344 };
345 write!(f, "{code_str}")
346 }
347}
348
349impl ErrorCode {
350 #[inline]
352 #[must_use]
353 pub const fn family_prefix(self) -> &'static str {
354 match self {
355 Self::CBKP001_SYNTAX
356 | Self::CBKP011_UNSUPPORTED_CLAUSE
357 | Self::CBKP021_ODO_NOT_TAIL
358 | Self::CBKP022_NESTED_ODO
359 | Self::CBKP023_ODO_REDEFINES
360 | Self::CBKP051_UNSUPPORTED_EDITED_PIC
361 | Self::CBKP101_INVALID_PIC => "CBKP",
362 Self::CBKS121_COUNTER_NOT_FOUND
363 | Self::CBKS141_RECORD_TOO_LARGE
364 | Self::CBKS301_ODO_CLIPPED
365 | Self::CBKS302_ODO_RAISED
366 | Self::CBKS601_RENAME_UNKNOWN_FROM
367 | Self::CBKS602_RENAME_UNKNOWN_THRU
368 | Self::CBKS603_RENAME_NOT_CONTIGUOUS
369 | Self::CBKS604_RENAME_REVERSED_RANGE
370 | Self::CBKS605_RENAME_FROM_CROSSES_GROUP
371 | Self::CBKS606_RENAME_THRU_CROSSES_GROUP
372 | Self::CBKS607_RENAME_CROSSES_OCCURS
373 | Self::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND
374 | Self::CBKS609_RENAME_OVER_REDEFINES
375 | Self::CBKS610_RENAME_MULTIPLE_REDEFINES
376 | Self::CBKS611_RENAME_PARTIAL_OCCURS
377 | Self::CBKS612_RENAME_ODO_NOT_SUPPORTED
378 | Self::CBKS701_PROJECTION_INVALID_ODO
379 | Self::CBKS702_PROJECTION_UNRESOLVED_ALIAS
380 | Self::CBKS703_PROJECTION_FIELD_NOT_FOUND => "CBKS",
381 Self::CBKR101_FIXED_RECORD_ERROR
382 | Self::CBKR201_RDW_READ_ERROR
383 | Self::CBKR211_RDW_RESERVED_NONZERO => "CBKR",
384 Self::CBKC201_JSON_WRITE_ERROR | Self::CBKC301_INVALID_EBCDIC_BYTE => "CBKC",
385 Self::CBKD101_INVALID_FIELD_TYPE
386 | Self::CBKD301_RECORD_TOO_SHORT
387 | Self::CBKD302_EDITED_PIC_NOT_IMPLEMENTED
388 | Self::CBKD401_COMP3_INVALID_NIBBLE
389 | Self::CBKD410_ZONED_OVERFLOW
390 | Self::CBKD411_ZONED_BAD_SIGN
391 | Self::CBKD412_ZONED_BLANK_IS_ZERO
392 | Self::CBKD413_ZONED_INVALID_ENCODING
393 | Self::CBKD414_ZONED_MIXED_ENCODING
394 | Self::CBKD415_ZONED_ENCODING_AMBIGUOUS
395 | Self::CBKD421_EDITED_PIC_INVALID_FORMAT
396 | Self::CBKD422_EDITED_PIC_SIGN_MISMATCH
397 | Self::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO
398 | Self::CBKD431_FLOAT_NAN
399 | Self::CBKD432_FLOAT_INFINITY => "CBKD",
400 Self::CBKI001_INVALID_STATE => "CBKI",
401 Self::CBKE501_JSON_TYPE_MISMATCH
402 | Self::CBKE505_SCALE_MISMATCH
403 | Self::CBKE510_NUMERIC_OVERFLOW
404 | Self::CBKE515_STRING_LENGTH_VIOLATION
405 | Self::CBKE521_ARRAY_LEN_OOB
406 | Self::CBKE530_SIGN_SEPARATE_ENCODE_ERROR
407 | Self::CBKE531_FLOAT_ENCODE_OVERFLOW => "CBKE",
408 Self::CBKF102_RECORD_LENGTH_INVALID
409 | Self::CBKF104_RDW_SUSPECT_ASCII
410 | Self::CBKF221_RDW_UNDERFLOW => "CBKF",
411 Self::CBKA001_BASELINE_ERROR => "CBKA",
412 Self::CBKW001_SCHEMA_CONVERSION
413 | Self::CBKW002_TYPE_MAPPING
414 | Self::CBKW003_DECIMAL_OVERFLOW
415 | Self::CBKW004_BATCH_BUILD
416 | Self::CBKW005_PARQUET_WRITE => "CBKW",
417 }
418 }
419}
420
421#[derive(Debug, Clone, PartialEq)]
427pub struct ErrorContext {
428 pub record_index: Option<u64>,
433
434 pub field_path: Option<String>,
439
440 pub byte_offset: Option<u64>,
444
445 pub line_number: Option<u32>,
449
450 pub details: Option<String>,
454}
455
456impl fmt::Display for ErrorContext {
457 #[inline]
458 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
459 let mut parts = Vec::new();
460
461 if let Some(record) = self.record_index {
462 parts.push(format!("record {record}"));
463 }
464 if let Some(ref path) = self.field_path {
465 parts.push(format!("field {path}"));
466 }
467 if let Some(offset) = self.byte_offset {
468 parts.push(format!("offset {offset}"));
469 }
470 if let Some(line) = self.line_number {
471 parts.push(format!("line {line}"));
472 }
473 if let Some(ref details) = self.details {
474 parts.push(details.clone());
475 }
476
477 write!(f, "{}", parts.join(", "))
478 }
479}
480
481impl Error {
482 #[inline]
510 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
511 Self {
512 code,
513 message: message.into(),
514 context: None,
515 }
516 }
517
518 #[must_use]
520 #[inline]
521 pub fn with_context(mut self, context: ErrorContext) -> Self {
522 self.context = Some(context);
523 self
524 }
525
526 #[must_use]
528 #[inline]
529 pub fn with_record(mut self, record_index: u64) -> Self {
530 let context = self.context.get_or_insert(ErrorContext {
531 record_index: None,
532 field_path: None,
533 byte_offset: None,
534 line_number: None,
535 details: None,
536 });
537 context.record_index = Some(record_index);
538 self
539 }
540
541 #[must_use]
543 #[inline]
544 pub fn with_field(mut self, field_path: impl Into<String>) -> Self {
545 let context = self.context.get_or_insert(ErrorContext {
546 record_index: None,
547 field_path: None,
548 byte_offset: None,
549 line_number: None,
550 details: None,
551 });
552 context.field_path = Some(field_path.into());
553 self
554 }
555
556 #[must_use]
558 #[inline]
559 pub fn with_offset(mut self, byte_offset: u64) -> Self {
560 let context = self.context.get_or_insert(ErrorContext {
561 record_index: None,
562 field_path: None,
563 byte_offset: None,
564 line_number: None,
565 details: None,
566 });
567 context.byte_offset = Some(byte_offset);
568 self
569 }
570}
571
572#[macro_export]
574macro_rules! error {
575 ($code:expr, $msg:expr) => {
576 $crate::Error::new($code, $msg)
577 };
578 ($code:expr, $fmt:expr, $($arg:tt)*) => {
579 $crate::Error::new($code, format!($fmt, $($arg)*))
580 };
581}
582
583#[cfg(test)]
584#[allow(clippy::expect_used)]
585#[allow(clippy::unwrap_used)] mod tests {
587 use super::*;
588
589 #[test]
590 fn test_error_code_serialization() {
591 let code = ErrorCode::CBKD411_ZONED_BAD_SIGN;
592 let json = serde_json::to_string(&code).unwrap();
593 assert_eq!(json, "\"CBKD411_ZONED_BAD_SIGN\"");
594
595 let deserialized: ErrorCode = serde_json::from_str(&json).unwrap();
596 assert_eq!(deserialized, code);
597 }
598
599 #[test]
600 fn test_error_display_format() {
601 let error = Error::new(
602 ErrorCode::CBKD411_ZONED_BAD_SIGN,
603 "Invalid sign zone in field",
604 );
605 let display = format!("{error}");
606 assert_eq!(
607 display,
608 "CBKD411_ZONED_BAD_SIGN: Invalid sign zone in field"
609 );
610 }
611
612 #[test]
613 fn test_error_with_context_display() {
614 let error =
615 Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Test error").with_field("AMOUNT");
616 let display = format!("{error}");
617 assert!(display.contains("CBKD411_ZONED_BAD_SIGN: Test error"));
618 assert!(display.contains("field AMOUNT"));
619 }
620
621 #[test]
622 fn test_error_static_message() {
623 let error = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Static message");
624 assert_eq!(error.message, "Static message");
625 assert_eq!(error.code, ErrorCode::CBKD411_ZONED_BAD_SIGN);
626 assert!(error.context.is_none());
627 }
628
629 #[test]
630 fn test_error_dynamic_message() {
631 let field = "AMOUNT";
632 let error = Error::new(
633 ErrorCode::CBKD411_ZONED_BAD_SIGN,
634 format!("Dynamic message for field {field}"),
635 );
636 assert_eq!(error.message, "Dynamic message for field AMOUNT");
637 }
638
639 #[test]
640 fn test_error_macro_static() {
641 let err = error!(ErrorCode::CBKP001_SYNTAX, "Empty copybook");
642 assert_eq!(err.code, ErrorCode::CBKP001_SYNTAX);
643 assert_eq!(err.message, "Empty copybook");
644 }
645
646 #[test]
647 fn test_error_macro_formatted() {
648 let field = "CUSTOMER_ID";
649 let err = error!(
650 ErrorCode::CBKD301_RECORD_TOO_SHORT,
651 "Field {} missing", field
652 );
653 assert_eq!(err.code, ErrorCode::CBKD301_RECORD_TOO_SHORT);
654 assert!(err.message.contains("CUSTOMER_ID"));
655 }
656
657 #[test]
662 fn test_error_code_accessor() {
663 let err = Error::new(ErrorCode::CBKE510_NUMERIC_OVERFLOW, "overflow");
664 assert_eq!(err.code(), ErrorCode::CBKE510_NUMERIC_OVERFLOW);
665 }
666
667 #[test]
668 fn test_error_family_prefix_via_error() {
669 let err = Error::new(ErrorCode::CBKR211_RDW_RESERVED_NONZERO, "reserved");
670 assert_eq!(err.family_prefix(), "CBKR");
671 }
672
673 #[test]
674 fn test_error_with_record_builder() {
675 let err = Error::new(ErrorCode::CBKD301_RECORD_TOO_SHORT, "short").with_record(7);
676 let ctx = err.context.as_ref().unwrap();
677 assert_eq!(ctx.record_index, Some(7));
678 assert!(ctx.field_path.is_none());
679 assert!(ctx.byte_offset.is_none());
680 }
681
682 #[test]
683 fn test_error_with_offset_builder() {
684 let err = Error::new(ErrorCode::CBKD301_RECORD_TOO_SHORT, "short").with_offset(128);
685 let ctx = err.context.as_ref().unwrap();
686 assert_eq!(ctx.byte_offset, Some(128));
687 assert!(ctx.record_index.is_none());
688 assert!(ctx.field_path.is_none());
689 }
690
691 #[test]
692 fn test_error_chained_context_builders() {
693 let err = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "bad nibble")
694 .with_record(10)
695 .with_field("CUSTOMER.BALANCE")
696 .with_offset(64);
697 let ctx = err.context.as_ref().unwrap();
698 assert_eq!(ctx.record_index, Some(10));
699 assert_eq!(ctx.field_path.as_deref(), Some("CUSTOMER.BALANCE"));
700 assert_eq!(ctx.byte_offset, Some(64));
701 assert!(ctx.line_number.is_none());
702 assert!(ctx.details.is_none());
703 }
704
705 #[test]
706 fn test_error_with_context_sets_full_context() {
707 let ctx = ErrorContext {
708 record_index: Some(99),
709 field_path: Some("ROOT.CHILD".into()),
710 byte_offset: Some(512),
711 line_number: Some(42),
712 details: Some("extra detail".into()),
713 };
714 let err = Error::new(ErrorCode::CBKP001_SYNTAX, "bad syntax").with_context(ctx);
715 let c = err.context.as_ref().unwrap();
716 assert_eq!(c.record_index, Some(99));
717 assert_eq!(c.field_path.as_deref(), Some("ROOT.CHILD"));
718 assert_eq!(c.byte_offset, Some(512));
719 assert_eq!(c.line_number, Some(42));
720 assert_eq!(c.details.as_deref(), Some("extra detail"));
721 }
722
723 #[test]
728 fn test_error_clone_equality() {
729 let err1 = Error::new(ErrorCode::CBKE501_JSON_TYPE_MISMATCH, "mismatch")
730 .with_field("AMOUNT")
731 .with_record(3);
732 let err2 = err1.clone();
733 assert_eq!(err1, err2);
734 assert_eq!(err1.code, err2.code);
735 assert_eq!(err1.message, err2.message);
736 assert_eq!(err1.context, err2.context);
737 }
738
739 #[test]
740 fn test_error_code_copy_clone_hash() {
741 use std::collections::HashSet;
742 let code = ErrorCode::CBKP001_SYNTAX;
743 let copy = code;
744 assert_eq!(code, copy);
745
746 let mut set = HashSet::new();
747 set.insert(ErrorCode::CBKP001_SYNTAX);
748 set.insert(ErrorCode::CBKD301_RECORD_TOO_SHORT);
749 set.insert(ErrorCode::CBKP001_SYNTAX); assert_eq!(set.len(), 2);
751 }
752
753 #[test]
754 fn test_error_implements_std_error() {
755 let err = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "bad sign");
756 let std_err: &dyn std::error::Error = &err;
757 assert!(std_err.source().is_none());
759 assert!(!std_err.to_string().is_empty());
760 }
761
762 #[test]
767 fn test_error_context_display_empty() {
768 let ctx = ErrorContext {
769 record_index: None,
770 field_path: None,
771 byte_offset: None,
772 line_number: None,
773 details: None,
774 };
775 assert_eq!(format!("{ctx}"), "");
776 }
777
778 #[test]
779 fn test_error_context_display_record_only() {
780 let ctx = ErrorContext {
781 record_index: Some(42),
782 field_path: None,
783 byte_offset: None,
784 line_number: None,
785 details: None,
786 };
787 assert_eq!(format!("{ctx}"), "record 42");
788 }
789
790 #[test]
791 fn test_error_context_display_line_number_only() {
792 let ctx = ErrorContext {
793 record_index: None,
794 field_path: None,
795 byte_offset: None,
796 line_number: Some(15),
797 details: None,
798 };
799 assert_eq!(format!("{ctx}"), "line 15");
800 }
801
802 #[test]
803 fn test_error_context_display_details_only() {
804 let ctx = ErrorContext {
805 record_index: None,
806 field_path: None,
807 byte_offset: None,
808 line_number: None,
809 details: Some("expected 8 bytes, got 4".into()),
810 };
811 assert_eq!(format!("{ctx}"), "expected 8 bytes, got 4");
812 }
813
814 #[test]
819 fn test_all_cbkp_codes_have_cbkp_prefix() {
820 let codes = [
821 ErrorCode::CBKP001_SYNTAX,
822 ErrorCode::CBKP011_UNSUPPORTED_CLAUSE,
823 ErrorCode::CBKP021_ODO_NOT_TAIL,
824 ErrorCode::CBKP022_NESTED_ODO,
825 ErrorCode::CBKP023_ODO_REDEFINES,
826 ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC,
827 ErrorCode::CBKP101_INVALID_PIC,
828 ];
829 for code in codes {
830 assert_eq!(code.family_prefix(), "CBKP", "failed for {code}");
831 }
832 }
833
834 #[test]
835 fn test_all_cbks_codes_have_cbks_prefix() {
836 let codes = [
837 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
838 ErrorCode::CBKS141_RECORD_TOO_LARGE,
839 ErrorCode::CBKS301_ODO_CLIPPED,
840 ErrorCode::CBKS302_ODO_RAISED,
841 ErrorCode::CBKS601_RENAME_UNKNOWN_FROM,
842 ErrorCode::CBKS602_RENAME_UNKNOWN_THRU,
843 ErrorCode::CBKS603_RENAME_NOT_CONTIGUOUS,
844 ErrorCode::CBKS604_RENAME_REVERSED_RANGE,
845 ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP,
846 ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP,
847 ErrorCode::CBKS607_RENAME_CROSSES_OCCURS,
848 ErrorCode::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND,
849 ErrorCode::CBKS609_RENAME_OVER_REDEFINES,
850 ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES,
851 ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS,
852 ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED,
853 ErrorCode::CBKS701_PROJECTION_INVALID_ODO,
854 ErrorCode::CBKS702_PROJECTION_UNRESOLVED_ALIAS,
855 ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND,
856 ];
857 for code in codes {
858 assert_eq!(code.family_prefix(), "CBKS", "failed for {code}");
859 }
860 }
861
862 #[test]
863 fn test_all_cbkd_codes_have_cbkd_prefix() {
864 let codes = [
865 ErrorCode::CBKD101_INVALID_FIELD_TYPE,
866 ErrorCode::CBKD301_RECORD_TOO_SHORT,
867 ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED,
868 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
869 ErrorCode::CBKD410_ZONED_OVERFLOW,
870 ErrorCode::CBKD411_ZONED_BAD_SIGN,
871 ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO,
872 ErrorCode::CBKD413_ZONED_INVALID_ENCODING,
873 ErrorCode::CBKD414_ZONED_MIXED_ENCODING,
874 ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS,
875 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
876 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
877 ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
878 ErrorCode::CBKD431_FLOAT_NAN,
879 ErrorCode::CBKD432_FLOAT_INFINITY,
880 ];
881 for code in codes {
882 assert_eq!(code.family_prefix(), "CBKD", "failed for {code}");
883 }
884 }
885
886 #[test]
887 fn test_all_cbke_codes_have_cbke_prefix() {
888 let codes = [
889 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
890 ErrorCode::CBKE505_SCALE_MISMATCH,
891 ErrorCode::CBKE510_NUMERIC_OVERFLOW,
892 ErrorCode::CBKE515_STRING_LENGTH_VIOLATION,
893 ErrorCode::CBKE521_ARRAY_LEN_OOB,
894 ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
895 ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW,
896 ];
897 for code in codes {
898 assert_eq!(code.family_prefix(), "CBKE", "failed for {code}");
899 }
900 }
901
902 #[test]
903 fn test_remaining_family_prefixes() {
904 assert_eq!(
905 ErrorCode::CBKR211_RDW_RESERVED_NONZERO.family_prefix(),
906 "CBKR"
907 );
908 assert_eq!(ErrorCode::CBKC201_JSON_WRITE_ERROR.family_prefix(), "CBKC");
909 assert_eq!(
910 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE.family_prefix(),
911 "CBKC"
912 );
913 assert_eq!(ErrorCode::CBKI001_INVALID_STATE.family_prefix(), "CBKI");
914 assert_eq!(
915 ErrorCode::CBKF102_RECORD_LENGTH_INVALID.family_prefix(),
916 "CBKF"
917 );
918 assert_eq!(ErrorCode::CBKF104_RDW_SUSPECT_ASCII.family_prefix(), "CBKF");
919 assert_eq!(ErrorCode::CBKF221_RDW_UNDERFLOW.family_prefix(), "CBKF");
920 assert_eq!(ErrorCode::CBKA001_BASELINE_ERROR.family_prefix(), "CBKA");
921 assert_eq!(ErrorCode::CBKW001_SCHEMA_CONVERSION.family_prefix(), "CBKW");
922 assert_eq!(ErrorCode::CBKW002_TYPE_MAPPING.family_prefix(), "CBKW");
923 assert_eq!(ErrorCode::CBKW003_DECIMAL_OVERFLOW.family_prefix(), "CBKW");
924 assert_eq!(ErrorCode::CBKW004_BATCH_BUILD.family_prefix(), "CBKW");
925 assert_eq!(ErrorCode::CBKW005_PARQUET_WRITE.family_prefix(), "CBKW");
926 }
927
928 #[test]
933 fn test_error_code_display_starts_with_family_prefix() {
934 let representative_codes = [
935 ErrorCode::CBKP001_SYNTAX,
936 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
937 ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
938 ErrorCode::CBKC201_JSON_WRITE_ERROR,
939 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
940 ErrorCode::CBKI001_INVALID_STATE,
941 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
942 ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
943 ErrorCode::CBKA001_BASELINE_ERROR,
944 ErrorCode::CBKW001_SCHEMA_CONVERSION,
945 ];
946 for code in representative_codes {
947 let display = format!("{code}");
948 let prefix = code.family_prefix();
949 assert!(
950 display.starts_with(prefix),
951 "{display} should start with {prefix}"
952 );
953 }
954 }
955
956 #[test]
961 fn test_error_code_serde_roundtrip_all_families() {
962 let codes = [
963 ErrorCode::CBKP001_SYNTAX,
964 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
965 ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
966 ErrorCode::CBKC201_JSON_WRITE_ERROR,
967 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
968 ErrorCode::CBKI001_INVALID_STATE,
969 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
970 ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
971 ErrorCode::CBKA001_BASELINE_ERROR,
972 ErrorCode::CBKW001_SCHEMA_CONVERSION,
973 ];
974 for code in codes {
975 let json = serde_json::to_string(&code).unwrap();
976 let roundtripped: ErrorCode = serde_json::from_str(&json).unwrap();
977 assert_eq!(roundtripped, code, "round-trip failed for {code}");
978 }
979 }
980
981 #[test]
982 fn test_error_code_deserialization_from_string() {
983 let json = r#""CBKP101_INVALID_PIC""#;
984 let code: ErrorCode = serde_json::from_str(json).unwrap();
985 assert_eq!(code, ErrorCode::CBKP101_INVALID_PIC);
986 }
987
988 #[test]
989 fn test_error_code_deserialization_invalid_rejects() {
990 let json = r#""NOT_A_REAL_CODE""#;
991 let result: std::result::Result<ErrorCode, _> = serde_json::from_str(json);
992 assert!(result.is_err());
993 }
994
995 #[test]
1000 fn test_error_macro_multiple_format_args() {
1001 let field = "AMOUNT";
1002 let expected = 8;
1003 let actual = 4;
1004 let err = error!(
1005 ErrorCode::CBKD301_RECORD_TOO_SHORT,
1006 "Field {} expected {} bytes, got {}", field, expected, actual
1007 );
1008 assert_eq!(err.message, "Field AMOUNT expected 8 bytes, got 4");
1009 }
1010}