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)]
100#[allow(non_camel_case_types)] pub enum ErrorCode {
102 CBKP001_SYNTAX,
107 CBKP011_UNSUPPORTED_CLAUSE,
109 CBKP021_ODO_NOT_TAIL,
111 CBKP022_NESTED_ODO,
113 CBKP023_ODO_REDEFINES,
115 CBKP051_UNSUPPORTED_EDITED_PIC,
117 CBKP101_INVALID_PIC,
119
120 CBKS121_COUNTER_NOT_FOUND,
125 CBKS141_RECORD_TOO_LARGE,
127 CBKS301_ODO_CLIPPED,
129 CBKS302_ODO_RAISED,
131 CBKS601_RENAME_UNKNOWN_FROM,
133 CBKS602_RENAME_UNKNOWN_THRU,
135 CBKS603_RENAME_NOT_CONTIGUOUS,
137 CBKS604_RENAME_REVERSED_RANGE,
139 CBKS605_RENAME_FROM_CROSSES_GROUP,
141 CBKS606_RENAME_THRU_CROSSES_GROUP,
143 CBKS607_RENAME_CROSSES_OCCURS,
145 CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND,
147 CBKS609_RENAME_OVER_REDEFINES,
149 CBKS610_RENAME_MULTIPLE_REDEFINES,
151 CBKS611_RENAME_PARTIAL_OCCURS,
153 CBKS612_RENAME_ODO_NOT_SUPPORTED,
155 CBKS701_PROJECTION_INVALID_ODO,
157 CBKS702_PROJECTION_UNRESOLVED_ALIAS,
159 CBKS703_PROJECTION_FIELD_NOT_FOUND,
161
162 CBKR211_RDW_RESERVED_NONZERO,
167
168 CBKC201_JSON_WRITE_ERROR,
173 CBKC301_INVALID_EBCDIC_BYTE,
175
176 CBKD101_INVALID_FIELD_TYPE,
181 CBKD301_RECORD_TOO_SHORT,
183 CBKD302_EDITED_PIC_NOT_IMPLEMENTED,
185 CBKD401_COMP3_INVALID_NIBBLE,
187 CBKD410_ZONED_OVERFLOW,
189 CBKD411_ZONED_BAD_SIGN,
191 CBKD412_ZONED_BLANK_IS_ZERO,
193 CBKD413_ZONED_INVALID_ENCODING,
195 CBKD414_ZONED_MIXED_ENCODING,
197 CBKD415_ZONED_ENCODING_AMBIGUOUS,
199 CBKD421_EDITED_PIC_INVALID_FORMAT,
201 CBKD422_EDITED_PIC_SIGN_MISMATCH,
203 CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
205 CBKD431_FLOAT_NAN,
207 CBKD432_FLOAT_INFINITY,
209
210 CBKI001_INVALID_STATE,
215
216 CBKE501_JSON_TYPE_MISMATCH,
221 CBKE505_SCALE_MISMATCH,
223 CBKE510_NUMERIC_OVERFLOW,
225 CBKE515_STRING_LENGTH_VIOLATION,
227 CBKE521_ARRAY_LEN_OOB,
229 CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
231 CBKE531_FLOAT_ENCODE_OVERFLOW,
233
234 CBKF102_RECORD_LENGTH_INVALID,
239 CBKF104_RDW_SUSPECT_ASCII,
241 CBKF221_RDW_UNDERFLOW,
243
244 CBKA001_BASELINE_ERROR,
249
250 CBKW001_SCHEMA_CONVERSION,
255 CBKW002_TYPE_MAPPING,
257 CBKW003_DECIMAL_OVERFLOW,
259 CBKW004_BATCH_BUILD,
261 CBKW005_PARQUET_WRITE,
263}
264
265impl fmt::Display for ErrorCode {
266 #[inline]
267 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 let code_str = match self {
269 ErrorCode::CBKP001_SYNTAX => "CBKP001_SYNTAX",
270 ErrorCode::CBKP011_UNSUPPORTED_CLAUSE => "CBKP011_UNSUPPORTED_CLAUSE",
271 ErrorCode::CBKP021_ODO_NOT_TAIL => "CBKP021_ODO_NOT_TAIL",
272 ErrorCode::CBKP022_NESTED_ODO => "CBKP022_NESTED_ODO",
273 ErrorCode::CBKP023_ODO_REDEFINES => "CBKP023_ODO_REDEFINES",
274 ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC => "CBKP051_UNSUPPORTED_EDITED_PIC",
275 ErrorCode::CBKP101_INVALID_PIC => "CBKP101_INVALID_PIC",
276 ErrorCode::CBKS121_COUNTER_NOT_FOUND => "CBKS121_COUNTER_NOT_FOUND",
277 ErrorCode::CBKS141_RECORD_TOO_LARGE => "CBKS141_RECORD_TOO_LARGE",
278 ErrorCode::CBKS301_ODO_CLIPPED => "CBKS301_ODO_CLIPPED",
279 ErrorCode::CBKS302_ODO_RAISED => "CBKS302_ODO_RAISED",
280 ErrorCode::CBKS601_RENAME_UNKNOWN_FROM => "CBKS601_RENAME_UNKNOWN_FROM",
281 ErrorCode::CBKS602_RENAME_UNKNOWN_THRU => "CBKS602_RENAME_UNKNOWN_THRU",
282 ErrorCode::CBKS603_RENAME_NOT_CONTIGUOUS => "CBKS603_RENAME_NOT_CONTIGUOUS",
283 ErrorCode::CBKS604_RENAME_REVERSED_RANGE => "CBKS604_RENAME_REVERSED_RANGE",
284 ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP => "CBKS605_RENAME_FROM_CROSSES_GROUP",
285 ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP => "CBKS606_RENAME_THRU_CROSSES_GROUP",
286 ErrorCode::CBKS607_RENAME_CROSSES_OCCURS => "CBKS607_RENAME_CROSSES_OCCURS",
287 ErrorCode::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND => {
288 "CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND"
289 }
290 ErrorCode::CBKS609_RENAME_OVER_REDEFINES => "CBKS609_RENAME_OVER_REDEFINES",
291 ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES => "CBKS610_RENAME_MULTIPLE_REDEFINES",
292 ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS => "CBKS611_RENAME_PARTIAL_OCCURS",
293 ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED => "CBKS612_RENAME_ODO_NOT_SUPPORTED",
294 ErrorCode::CBKS701_PROJECTION_INVALID_ODO => "CBKS701_PROJECTION_INVALID_ODO",
295 ErrorCode::CBKS702_PROJECTION_UNRESOLVED_ALIAS => "CBKS702_PROJECTION_UNRESOLVED_ALIAS",
296 ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND => "CBKS703_PROJECTION_FIELD_NOT_FOUND",
297 ErrorCode::CBKR211_RDW_RESERVED_NONZERO => "CBKR211_RDW_RESERVED_NONZERO",
298 ErrorCode::CBKC201_JSON_WRITE_ERROR => "CBKC201_JSON_WRITE_ERROR",
299 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE => "CBKC301_INVALID_EBCDIC_BYTE",
300 ErrorCode::CBKD101_INVALID_FIELD_TYPE => "CBKD101_INVALID_FIELD_TYPE",
301 ErrorCode::CBKD301_RECORD_TOO_SHORT => "CBKD301_RECORD_TOO_SHORT",
302 ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED => "CBKD302_EDITED_PIC_NOT_IMPLEMENTED",
303 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE => "CBKD401_COMP3_INVALID_NIBBLE",
304 ErrorCode::CBKD410_ZONED_OVERFLOW => "CBKD410_ZONED_OVERFLOW",
305 ErrorCode::CBKD411_ZONED_BAD_SIGN => "CBKD411_ZONED_BAD_SIGN",
306 ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO => "CBKD412_ZONED_BLANK_IS_ZERO",
307 ErrorCode::CBKD413_ZONED_INVALID_ENCODING => "CBKD413_ZONED_INVALID_ENCODING",
308 ErrorCode::CBKD414_ZONED_MIXED_ENCODING => "CBKD414_ZONED_MIXED_ENCODING",
309 ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS => "CBKD415_ZONED_ENCODING_AMBIGUOUS",
310 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT => "CBKD421_EDITED_PIC_INVALID_FORMAT",
311 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH => "CBKD422_EDITED_PIC_SIGN_MISMATCH",
312 ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO => "CBKD423_EDITED_PIC_BLANK_WHEN_ZERO",
313 ErrorCode::CBKD431_FLOAT_NAN => "CBKD431_FLOAT_NAN",
314 ErrorCode::CBKD432_FLOAT_INFINITY => "CBKD432_FLOAT_INFINITY",
315 ErrorCode::CBKI001_INVALID_STATE => "CBKI001_INVALID_STATE",
316 ErrorCode::CBKE501_JSON_TYPE_MISMATCH => "CBKE501_JSON_TYPE_MISMATCH",
317 ErrorCode::CBKE505_SCALE_MISMATCH => "CBKE505_SCALE_MISMATCH",
318 ErrorCode::CBKE510_NUMERIC_OVERFLOW => "CBKE510_NUMERIC_OVERFLOW",
319 ErrorCode::CBKE515_STRING_LENGTH_VIOLATION => "CBKE515_STRING_LENGTH_VIOLATION",
320 ErrorCode::CBKE521_ARRAY_LEN_OOB => "CBKE521_ARRAY_LEN_OOB",
321 ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR => "CBKE530_SIGN_SEPARATE_ENCODE_ERROR",
322 ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW => "CBKE531_FLOAT_ENCODE_OVERFLOW",
323 ErrorCode::CBKF102_RECORD_LENGTH_INVALID => "CBKF102_RECORD_LENGTH_INVALID",
324 ErrorCode::CBKF104_RDW_SUSPECT_ASCII => "CBKF104_RDW_SUSPECT_ASCII",
325 ErrorCode::CBKF221_RDW_UNDERFLOW => "CBKF221_RDW_UNDERFLOW",
326 ErrorCode::CBKA001_BASELINE_ERROR => "CBKA001_BASELINE_ERROR",
327 ErrorCode::CBKW001_SCHEMA_CONVERSION => "CBKW001_SCHEMA_CONVERSION",
328 ErrorCode::CBKW002_TYPE_MAPPING => "CBKW002_TYPE_MAPPING",
329 ErrorCode::CBKW003_DECIMAL_OVERFLOW => "CBKW003_DECIMAL_OVERFLOW",
330 ErrorCode::CBKW004_BATCH_BUILD => "CBKW004_BATCH_BUILD",
331 ErrorCode::CBKW005_PARQUET_WRITE => "CBKW005_PARQUET_WRITE",
332 };
333 write!(f, "{code_str}")
334 }
335}
336
337impl ErrorCode {
338 #[inline]
340 #[must_use]
341 pub const fn family_prefix(self) -> &'static str {
342 match self {
343 Self::CBKP001_SYNTAX
344 | Self::CBKP011_UNSUPPORTED_CLAUSE
345 | Self::CBKP021_ODO_NOT_TAIL
346 | Self::CBKP022_NESTED_ODO
347 | Self::CBKP023_ODO_REDEFINES
348 | Self::CBKP051_UNSUPPORTED_EDITED_PIC
349 | Self::CBKP101_INVALID_PIC => "CBKP",
350 Self::CBKS121_COUNTER_NOT_FOUND
351 | Self::CBKS141_RECORD_TOO_LARGE
352 | Self::CBKS301_ODO_CLIPPED
353 | Self::CBKS302_ODO_RAISED
354 | Self::CBKS601_RENAME_UNKNOWN_FROM
355 | Self::CBKS602_RENAME_UNKNOWN_THRU
356 | Self::CBKS603_RENAME_NOT_CONTIGUOUS
357 | Self::CBKS604_RENAME_REVERSED_RANGE
358 | Self::CBKS605_RENAME_FROM_CROSSES_GROUP
359 | Self::CBKS606_RENAME_THRU_CROSSES_GROUP
360 | Self::CBKS607_RENAME_CROSSES_OCCURS
361 | Self::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND
362 | Self::CBKS609_RENAME_OVER_REDEFINES
363 | Self::CBKS610_RENAME_MULTIPLE_REDEFINES
364 | Self::CBKS611_RENAME_PARTIAL_OCCURS
365 | Self::CBKS612_RENAME_ODO_NOT_SUPPORTED
366 | Self::CBKS701_PROJECTION_INVALID_ODO
367 | Self::CBKS702_PROJECTION_UNRESOLVED_ALIAS
368 | Self::CBKS703_PROJECTION_FIELD_NOT_FOUND => "CBKS",
369 Self::CBKR211_RDW_RESERVED_NONZERO => "CBKR",
370 Self::CBKC201_JSON_WRITE_ERROR | Self::CBKC301_INVALID_EBCDIC_BYTE => "CBKC",
371 Self::CBKD101_INVALID_FIELD_TYPE
372 | Self::CBKD301_RECORD_TOO_SHORT
373 | Self::CBKD302_EDITED_PIC_NOT_IMPLEMENTED
374 | Self::CBKD401_COMP3_INVALID_NIBBLE
375 | Self::CBKD410_ZONED_OVERFLOW
376 | Self::CBKD411_ZONED_BAD_SIGN
377 | Self::CBKD412_ZONED_BLANK_IS_ZERO
378 | Self::CBKD413_ZONED_INVALID_ENCODING
379 | Self::CBKD414_ZONED_MIXED_ENCODING
380 | Self::CBKD415_ZONED_ENCODING_AMBIGUOUS
381 | Self::CBKD421_EDITED_PIC_INVALID_FORMAT
382 | Self::CBKD422_EDITED_PIC_SIGN_MISMATCH
383 | Self::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO
384 | Self::CBKD431_FLOAT_NAN
385 | Self::CBKD432_FLOAT_INFINITY => "CBKD",
386 Self::CBKI001_INVALID_STATE => "CBKI",
387 Self::CBKE501_JSON_TYPE_MISMATCH
388 | Self::CBKE505_SCALE_MISMATCH
389 | Self::CBKE510_NUMERIC_OVERFLOW
390 | Self::CBKE515_STRING_LENGTH_VIOLATION
391 | Self::CBKE521_ARRAY_LEN_OOB
392 | Self::CBKE530_SIGN_SEPARATE_ENCODE_ERROR
393 | Self::CBKE531_FLOAT_ENCODE_OVERFLOW => "CBKE",
394 Self::CBKF102_RECORD_LENGTH_INVALID
395 | Self::CBKF104_RDW_SUSPECT_ASCII
396 | Self::CBKF221_RDW_UNDERFLOW => "CBKF",
397 Self::CBKA001_BASELINE_ERROR => "CBKA",
398 Self::CBKW001_SCHEMA_CONVERSION
399 | Self::CBKW002_TYPE_MAPPING
400 | Self::CBKW003_DECIMAL_OVERFLOW
401 | Self::CBKW004_BATCH_BUILD
402 | Self::CBKW005_PARQUET_WRITE => "CBKW",
403 }
404 }
405}
406
407#[derive(Debug, Clone, PartialEq)]
413pub struct ErrorContext {
414 pub record_index: Option<u64>,
419
420 pub field_path: Option<String>,
425
426 pub byte_offset: Option<u64>,
430
431 pub line_number: Option<u32>,
435
436 pub details: Option<String>,
440}
441
442impl fmt::Display for ErrorContext {
443 #[inline]
444 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
445 let mut parts = Vec::new();
446
447 if let Some(record) = self.record_index {
448 parts.push(format!("record {record}"));
449 }
450 if let Some(ref path) = self.field_path {
451 parts.push(format!("field {path}"));
452 }
453 if let Some(offset) = self.byte_offset {
454 parts.push(format!("offset {offset}"));
455 }
456 if let Some(line) = self.line_number {
457 parts.push(format!("line {line}"));
458 }
459 if let Some(ref details) = self.details {
460 parts.push(details.clone());
461 }
462
463 write!(f, "{}", parts.join(", "))
464 }
465}
466
467impl Error {
468 #[inline]
496 pub fn new(code: ErrorCode, message: impl Into<String>) -> Self {
497 Self {
498 code,
499 message: message.into(),
500 context: None,
501 }
502 }
503
504 #[must_use]
506 #[inline]
507 pub fn with_context(mut self, context: ErrorContext) -> Self {
508 self.context = Some(context);
509 self
510 }
511
512 #[must_use]
514 #[inline]
515 pub fn with_record(mut self, record_index: u64) -> Self {
516 let context = self.context.get_or_insert(ErrorContext {
517 record_index: None,
518 field_path: None,
519 byte_offset: None,
520 line_number: None,
521 details: None,
522 });
523 context.record_index = Some(record_index);
524 self
525 }
526
527 #[must_use]
529 #[inline]
530 pub fn with_field(mut self, field_path: impl Into<String>) -> Self {
531 let context = self.context.get_or_insert(ErrorContext {
532 record_index: None,
533 field_path: None,
534 byte_offset: None,
535 line_number: None,
536 details: None,
537 });
538 context.field_path = Some(field_path.into());
539 self
540 }
541
542 #[must_use]
544 #[inline]
545 pub fn with_offset(mut self, byte_offset: u64) -> Self {
546 let context = self.context.get_or_insert(ErrorContext {
547 record_index: None,
548 field_path: None,
549 byte_offset: None,
550 line_number: None,
551 details: None,
552 });
553 context.byte_offset = Some(byte_offset);
554 self
555 }
556}
557
558#[macro_export]
560macro_rules! error {
561 ($code:expr, $msg:expr) => {
562 $crate::Error::new($code, $msg)
563 };
564 ($code:expr, $fmt:expr, $($arg:tt)*) => {
565 $crate::Error::new($code, format!($fmt, $($arg)*))
566 };
567}
568
569#[cfg(test)]
570#[allow(clippy::expect_used)]
571#[allow(clippy::unwrap_used)] mod tests {
573 use super::*;
574
575 #[test]
576 fn test_error_code_serialization() {
577 let code = ErrorCode::CBKD411_ZONED_BAD_SIGN;
578 let json = serde_json::to_string(&code).unwrap();
579 assert_eq!(json, "\"CBKD411_ZONED_BAD_SIGN\"");
580
581 let deserialized: ErrorCode = serde_json::from_str(&json).unwrap();
582 assert_eq!(deserialized, code);
583 }
584
585 #[test]
586 fn test_error_display_format() {
587 let error = Error::new(
588 ErrorCode::CBKD411_ZONED_BAD_SIGN,
589 "Invalid sign zone in field",
590 );
591 let display = format!("{error}");
592 assert_eq!(
593 display,
594 "CBKD411_ZONED_BAD_SIGN: Invalid sign zone in field"
595 );
596 }
597
598 #[test]
599 fn test_error_with_context_display() {
600 let error =
601 Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Test error").with_field("AMOUNT");
602 let display = format!("{error}");
603 assert!(display.contains("CBKD411_ZONED_BAD_SIGN: Test error"));
604 assert!(display.contains("field AMOUNT"));
605 }
606
607 #[test]
608 fn test_error_static_message() {
609 let error = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "Static message");
610 assert_eq!(error.message, "Static message");
611 assert_eq!(error.code, ErrorCode::CBKD411_ZONED_BAD_SIGN);
612 assert!(error.context.is_none());
613 }
614
615 #[test]
616 fn test_error_dynamic_message() {
617 let field = "AMOUNT";
618 let error = Error::new(
619 ErrorCode::CBKD411_ZONED_BAD_SIGN,
620 format!("Dynamic message for field {field}"),
621 );
622 assert_eq!(error.message, "Dynamic message for field AMOUNT");
623 }
624
625 #[test]
626 fn test_error_macro_static() {
627 let err = error!(ErrorCode::CBKP001_SYNTAX, "Empty copybook");
628 assert_eq!(err.code, ErrorCode::CBKP001_SYNTAX);
629 assert_eq!(err.message, "Empty copybook");
630 }
631
632 #[test]
633 fn test_error_macro_formatted() {
634 let field = "CUSTOMER_ID";
635 let err = error!(
636 ErrorCode::CBKD301_RECORD_TOO_SHORT,
637 "Field {} missing", field
638 );
639 assert_eq!(err.code, ErrorCode::CBKD301_RECORD_TOO_SHORT);
640 assert!(err.message.contains("CUSTOMER_ID"));
641 }
642
643 #[test]
648 fn test_error_code_accessor() {
649 let err = Error::new(ErrorCode::CBKE510_NUMERIC_OVERFLOW, "overflow");
650 assert_eq!(err.code(), ErrorCode::CBKE510_NUMERIC_OVERFLOW);
651 }
652
653 #[test]
654 fn test_error_family_prefix_via_error() {
655 let err = Error::new(ErrorCode::CBKR211_RDW_RESERVED_NONZERO, "reserved");
656 assert_eq!(err.family_prefix(), "CBKR");
657 }
658
659 #[test]
660 fn test_error_with_record_builder() {
661 let err = Error::new(ErrorCode::CBKD301_RECORD_TOO_SHORT, "short").with_record(7);
662 let ctx = err.context.as_ref().unwrap();
663 assert_eq!(ctx.record_index, Some(7));
664 assert!(ctx.field_path.is_none());
665 assert!(ctx.byte_offset.is_none());
666 }
667
668 #[test]
669 fn test_error_with_offset_builder() {
670 let err = Error::new(ErrorCode::CBKD301_RECORD_TOO_SHORT, "short").with_offset(128);
671 let ctx = err.context.as_ref().unwrap();
672 assert_eq!(ctx.byte_offset, Some(128));
673 assert!(ctx.record_index.is_none());
674 assert!(ctx.field_path.is_none());
675 }
676
677 #[test]
678 fn test_error_chained_context_builders() {
679 let err = Error::new(ErrorCode::CBKD401_COMP3_INVALID_NIBBLE, "bad nibble")
680 .with_record(10)
681 .with_field("CUSTOMER.BALANCE")
682 .with_offset(64);
683 let ctx = err.context.as_ref().unwrap();
684 assert_eq!(ctx.record_index, Some(10));
685 assert_eq!(ctx.field_path.as_deref(), Some("CUSTOMER.BALANCE"));
686 assert_eq!(ctx.byte_offset, Some(64));
687 assert!(ctx.line_number.is_none());
688 assert!(ctx.details.is_none());
689 }
690
691 #[test]
692 fn test_error_with_context_sets_full_context() {
693 let ctx = ErrorContext {
694 record_index: Some(99),
695 field_path: Some("ROOT.CHILD".into()),
696 byte_offset: Some(512),
697 line_number: Some(42),
698 details: Some("extra detail".into()),
699 };
700 let err = Error::new(ErrorCode::CBKP001_SYNTAX, "bad syntax").with_context(ctx);
701 let c = err.context.as_ref().unwrap();
702 assert_eq!(c.record_index, Some(99));
703 assert_eq!(c.field_path.as_deref(), Some("ROOT.CHILD"));
704 assert_eq!(c.byte_offset, Some(512));
705 assert_eq!(c.line_number, Some(42));
706 assert_eq!(c.details.as_deref(), Some("extra detail"));
707 }
708
709 #[test]
714 fn test_error_clone_equality() {
715 let err1 = Error::new(ErrorCode::CBKE501_JSON_TYPE_MISMATCH, "mismatch")
716 .with_field("AMOUNT")
717 .with_record(3);
718 let err2 = err1.clone();
719 assert_eq!(err1, err2);
720 assert_eq!(err1.code, err2.code);
721 assert_eq!(err1.message, err2.message);
722 assert_eq!(err1.context, err2.context);
723 }
724
725 #[test]
726 fn test_error_code_copy_clone_hash() {
727 use std::collections::HashSet;
728 let code = ErrorCode::CBKP001_SYNTAX;
729 let copy = code;
730 assert_eq!(code, copy);
731
732 let mut set = HashSet::new();
733 set.insert(ErrorCode::CBKP001_SYNTAX);
734 set.insert(ErrorCode::CBKD301_RECORD_TOO_SHORT);
735 set.insert(ErrorCode::CBKP001_SYNTAX); assert_eq!(set.len(), 2);
737 }
738
739 #[test]
740 fn test_error_implements_std_error() {
741 let err = Error::new(ErrorCode::CBKD411_ZONED_BAD_SIGN, "bad sign");
742 let std_err: &dyn std::error::Error = &err;
743 assert!(std_err.source().is_none());
745 assert!(!std_err.to_string().is_empty());
746 }
747
748 #[test]
753 fn test_error_context_display_empty() {
754 let ctx = ErrorContext {
755 record_index: None,
756 field_path: None,
757 byte_offset: None,
758 line_number: None,
759 details: None,
760 };
761 assert_eq!(format!("{ctx}"), "");
762 }
763
764 #[test]
765 fn test_error_context_display_record_only() {
766 let ctx = ErrorContext {
767 record_index: Some(42),
768 field_path: None,
769 byte_offset: None,
770 line_number: None,
771 details: None,
772 };
773 assert_eq!(format!("{ctx}"), "record 42");
774 }
775
776 #[test]
777 fn test_error_context_display_line_number_only() {
778 let ctx = ErrorContext {
779 record_index: None,
780 field_path: None,
781 byte_offset: None,
782 line_number: Some(15),
783 details: None,
784 };
785 assert_eq!(format!("{ctx}"), "line 15");
786 }
787
788 #[test]
789 fn test_error_context_display_details_only() {
790 let ctx = ErrorContext {
791 record_index: None,
792 field_path: None,
793 byte_offset: None,
794 line_number: None,
795 details: Some("expected 8 bytes, got 4".into()),
796 };
797 assert_eq!(format!("{ctx}"), "expected 8 bytes, got 4");
798 }
799
800 #[test]
805 fn test_all_cbkp_codes_have_cbkp_prefix() {
806 let codes = [
807 ErrorCode::CBKP001_SYNTAX,
808 ErrorCode::CBKP011_UNSUPPORTED_CLAUSE,
809 ErrorCode::CBKP021_ODO_NOT_TAIL,
810 ErrorCode::CBKP022_NESTED_ODO,
811 ErrorCode::CBKP023_ODO_REDEFINES,
812 ErrorCode::CBKP051_UNSUPPORTED_EDITED_PIC,
813 ErrorCode::CBKP101_INVALID_PIC,
814 ];
815 for code in codes {
816 assert_eq!(code.family_prefix(), "CBKP", "failed for {code}");
817 }
818 }
819
820 #[test]
821 fn test_all_cbks_codes_have_cbks_prefix() {
822 let codes = [
823 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
824 ErrorCode::CBKS141_RECORD_TOO_LARGE,
825 ErrorCode::CBKS301_ODO_CLIPPED,
826 ErrorCode::CBKS302_ODO_RAISED,
827 ErrorCode::CBKS601_RENAME_UNKNOWN_FROM,
828 ErrorCode::CBKS602_RENAME_UNKNOWN_THRU,
829 ErrorCode::CBKS603_RENAME_NOT_CONTIGUOUS,
830 ErrorCode::CBKS604_RENAME_REVERSED_RANGE,
831 ErrorCode::CBKS605_RENAME_FROM_CROSSES_GROUP,
832 ErrorCode::CBKS606_RENAME_THRU_CROSSES_GROUP,
833 ErrorCode::CBKS607_RENAME_CROSSES_OCCURS,
834 ErrorCode::CBKS608_RENAME_QUALIFIED_NAME_NOT_FOUND,
835 ErrorCode::CBKS609_RENAME_OVER_REDEFINES,
836 ErrorCode::CBKS610_RENAME_MULTIPLE_REDEFINES,
837 ErrorCode::CBKS611_RENAME_PARTIAL_OCCURS,
838 ErrorCode::CBKS612_RENAME_ODO_NOT_SUPPORTED,
839 ErrorCode::CBKS701_PROJECTION_INVALID_ODO,
840 ErrorCode::CBKS702_PROJECTION_UNRESOLVED_ALIAS,
841 ErrorCode::CBKS703_PROJECTION_FIELD_NOT_FOUND,
842 ];
843 for code in codes {
844 assert_eq!(code.family_prefix(), "CBKS", "failed for {code}");
845 }
846 }
847
848 #[test]
849 fn test_all_cbkd_codes_have_cbkd_prefix() {
850 let codes = [
851 ErrorCode::CBKD101_INVALID_FIELD_TYPE,
852 ErrorCode::CBKD301_RECORD_TOO_SHORT,
853 ErrorCode::CBKD302_EDITED_PIC_NOT_IMPLEMENTED,
854 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
855 ErrorCode::CBKD410_ZONED_OVERFLOW,
856 ErrorCode::CBKD411_ZONED_BAD_SIGN,
857 ErrorCode::CBKD412_ZONED_BLANK_IS_ZERO,
858 ErrorCode::CBKD413_ZONED_INVALID_ENCODING,
859 ErrorCode::CBKD414_ZONED_MIXED_ENCODING,
860 ErrorCode::CBKD415_ZONED_ENCODING_AMBIGUOUS,
861 ErrorCode::CBKD421_EDITED_PIC_INVALID_FORMAT,
862 ErrorCode::CBKD422_EDITED_PIC_SIGN_MISMATCH,
863 ErrorCode::CBKD423_EDITED_PIC_BLANK_WHEN_ZERO,
864 ErrorCode::CBKD431_FLOAT_NAN,
865 ErrorCode::CBKD432_FLOAT_INFINITY,
866 ];
867 for code in codes {
868 assert_eq!(code.family_prefix(), "CBKD", "failed for {code}");
869 }
870 }
871
872 #[test]
873 fn test_all_cbke_codes_have_cbke_prefix() {
874 let codes = [
875 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
876 ErrorCode::CBKE505_SCALE_MISMATCH,
877 ErrorCode::CBKE510_NUMERIC_OVERFLOW,
878 ErrorCode::CBKE515_STRING_LENGTH_VIOLATION,
879 ErrorCode::CBKE521_ARRAY_LEN_OOB,
880 ErrorCode::CBKE530_SIGN_SEPARATE_ENCODE_ERROR,
881 ErrorCode::CBKE531_FLOAT_ENCODE_OVERFLOW,
882 ];
883 for code in codes {
884 assert_eq!(code.family_prefix(), "CBKE", "failed for {code}");
885 }
886 }
887
888 #[test]
889 fn test_remaining_family_prefixes() {
890 assert_eq!(
891 ErrorCode::CBKR211_RDW_RESERVED_NONZERO.family_prefix(),
892 "CBKR"
893 );
894 assert_eq!(ErrorCode::CBKC201_JSON_WRITE_ERROR.family_prefix(), "CBKC");
895 assert_eq!(
896 ErrorCode::CBKC301_INVALID_EBCDIC_BYTE.family_prefix(),
897 "CBKC"
898 );
899 assert_eq!(ErrorCode::CBKI001_INVALID_STATE.family_prefix(), "CBKI");
900 assert_eq!(
901 ErrorCode::CBKF102_RECORD_LENGTH_INVALID.family_prefix(),
902 "CBKF"
903 );
904 assert_eq!(ErrorCode::CBKF104_RDW_SUSPECT_ASCII.family_prefix(), "CBKF");
905 assert_eq!(ErrorCode::CBKF221_RDW_UNDERFLOW.family_prefix(), "CBKF");
906 assert_eq!(ErrorCode::CBKA001_BASELINE_ERROR.family_prefix(), "CBKA");
907 assert_eq!(ErrorCode::CBKW001_SCHEMA_CONVERSION.family_prefix(), "CBKW");
908 assert_eq!(ErrorCode::CBKW002_TYPE_MAPPING.family_prefix(), "CBKW");
909 assert_eq!(ErrorCode::CBKW003_DECIMAL_OVERFLOW.family_prefix(), "CBKW");
910 assert_eq!(ErrorCode::CBKW004_BATCH_BUILD.family_prefix(), "CBKW");
911 assert_eq!(ErrorCode::CBKW005_PARQUET_WRITE.family_prefix(), "CBKW");
912 }
913
914 #[test]
919 fn test_error_code_display_starts_with_family_prefix() {
920 let representative_codes = [
921 ErrorCode::CBKP001_SYNTAX,
922 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
923 ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
924 ErrorCode::CBKC201_JSON_WRITE_ERROR,
925 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
926 ErrorCode::CBKI001_INVALID_STATE,
927 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
928 ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
929 ErrorCode::CBKA001_BASELINE_ERROR,
930 ErrorCode::CBKW001_SCHEMA_CONVERSION,
931 ];
932 for code in representative_codes {
933 let display = format!("{code}");
934 let prefix = code.family_prefix();
935 assert!(
936 display.starts_with(prefix),
937 "{display} should start with {prefix}"
938 );
939 }
940 }
941
942 #[test]
947 fn test_error_code_serde_roundtrip_all_families() {
948 let codes = [
949 ErrorCode::CBKP001_SYNTAX,
950 ErrorCode::CBKS121_COUNTER_NOT_FOUND,
951 ErrorCode::CBKR211_RDW_RESERVED_NONZERO,
952 ErrorCode::CBKC201_JSON_WRITE_ERROR,
953 ErrorCode::CBKD401_COMP3_INVALID_NIBBLE,
954 ErrorCode::CBKI001_INVALID_STATE,
955 ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
956 ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
957 ErrorCode::CBKA001_BASELINE_ERROR,
958 ErrorCode::CBKW001_SCHEMA_CONVERSION,
959 ];
960 for code in codes {
961 let json = serde_json::to_string(&code).unwrap();
962 let roundtripped: ErrorCode = serde_json::from_str(&json).unwrap();
963 assert_eq!(roundtripped, code, "round-trip failed for {code}");
964 }
965 }
966
967 #[test]
968 fn test_error_code_deserialization_from_string() {
969 let json = r#""CBKP101_INVALID_PIC""#;
970 let code: ErrorCode = serde_json::from_str(json).unwrap();
971 assert_eq!(code, ErrorCode::CBKP101_INVALID_PIC);
972 }
973
974 #[test]
975 fn test_error_code_deserialization_invalid_rejects() {
976 let json = r#""NOT_A_REAL_CODE""#;
977 let result: std::result::Result<ErrorCode, _> = serde_json::from_str(json);
978 assert!(result.is_err());
979 }
980
981 #[test]
986 fn test_error_macro_multiple_format_args() {
987 let field = "AMOUNT";
988 let expected = 8;
989 let actual = 4;
990 let err = error!(
991 ErrorCode::CBKD301_RECORD_TOO_SHORT,
992 "Field {} expected {} bytes, got {}", field, expected, actual
993 );
994 assert_eq!(err.message, "Field AMOUNT expected 8 bytes, got 4");
995 }
996}