1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2#![allow(clippy::missing_inline_in_public_items)]
9
10pub use copybook_charset::Codepage;
13pub use copybook_charset::UnmappablePolicy;
15pub use copybook_zoned_format::ZonedEncodingFormat;
17use serde::{Deserialize, Serialize};
18use std::fmt;
19
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum, Default)]
35pub enum FloatFormat {
36 #[default]
38 #[value(name = "ieee-be", alias = "ieee", alias = "ieee-big-endian")]
39 IeeeBigEndian,
40 #[value(name = "ibm-hex", alias = "ibm")]
42 IbmHex,
43}
44
45#[derive(Debug, Clone, Serialize, Deserialize)]
47#[allow(clippy::struct_excessive_bools)] pub struct DecodeOptions {
49 pub format: RecordFormat,
51 pub codepage: Codepage,
53 pub json_number_mode: JsonNumberMode,
55 pub emit_filler: bool,
57 pub emit_meta: bool,
59 pub emit_raw: RawMode,
61 pub strict_mode: bool,
63 pub max_errors: Option<u64>,
65 pub on_decode_unmappable: UnmappablePolicy,
67 pub threads: usize,
69 pub preserve_zoned_encoding: bool,
75 pub preferred_zoned_encoding: ZonedEncodingFormat,
80 #[serde(default)]
82 pub float_format: FloatFormat,
83}
84
85#[derive(Debug, Clone, Serialize, Deserialize)]
87#[allow(clippy::struct_excessive_bools)]
88pub struct EncodeOptions {
89 pub format: RecordFormat,
91 pub codepage: Codepage,
93 pub preferred_zoned_encoding: ZonedEncodingFormat,
95 pub use_raw: bool,
97 pub bwz_encode: bool,
99 pub strict_mode: bool,
101 pub max_errors: Option<u64>,
103 pub threads: usize,
105 pub coerce_numbers: bool,
107 pub on_encode_unmappable: UnmappablePolicy,
109 pub json_number_mode: JsonNumberMode,
111 pub zoned_encoding_override: Option<ZonedEncodingFormat>,
120 #[serde(default)]
122 pub float_format: FloatFormat,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
141pub enum RecordFormat {
142 Fixed,
144 RDW,
146}
147
148impl RecordFormat {
149 #[must_use]
151 pub const fn is_fixed(self) -> bool {
152 matches!(self, Self::Fixed)
153 }
154
155 #[must_use]
157 pub const fn is_variable(self) -> bool {
158 matches!(self, Self::RDW)
159 }
160
161 #[must_use]
163 pub const fn description(self) -> &'static str {
164 match self {
165 Self::Fixed => "Fixed-length records",
166 Self::RDW => "Variable-length records with Record Descriptor Word",
167 }
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
187pub enum JsonNumberMode {
188 Lossless,
190 Native,
192}
193
194impl JsonNumberMode {
195 #[must_use]
197 #[inline]
198 pub const fn is_lossless(self) -> bool {
199 matches!(self, Self::Lossless)
200 }
201
202 #[must_use]
204 #[inline]
205 pub const fn is_native(self) -> bool {
206 matches!(self, Self::Native)
207 }
208
209 #[must_use]
211 #[inline]
212 pub const fn description(self) -> &'static str {
213 match self {
214 Self::Lossless => "Lossless string representation for decimals",
215 Self::Native => "Native JSON numbers where possible",
216 }
217 }
218}
219
220#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
240pub enum RawMode {
241 Off,
243 Record,
245 Field,
247 #[value(name = "record+rdw")]
249 RecordRDW,
250}
251
252impl Default for DecodeOptions {
253 fn default() -> Self {
254 Self {
255 format: RecordFormat::Fixed,
256 codepage: Codepage::CP037,
257 json_number_mode: JsonNumberMode::Lossless,
258 emit_filler: false,
259 emit_meta: false,
260 emit_raw: RawMode::Off,
261 strict_mode: false,
262 max_errors: None,
263 on_decode_unmappable: UnmappablePolicy::Error,
264 threads: 1,
265 preserve_zoned_encoding: false,
266 preferred_zoned_encoding: ZonedEncodingFormat::Auto,
267 float_format: FloatFormat::IeeeBigEndian,
268 }
269 }
270}
271
272impl DecodeOptions {
273 #[must_use]
299 #[inline]
300 pub fn new() -> Self {
301 Self::default()
302 }
303
304 #[must_use]
306 #[inline]
307 pub fn with_format(mut self, format: RecordFormat) -> Self {
308 self.format = format;
309 self
310 }
311
312 #[must_use]
314 #[inline]
315 pub fn with_codepage(mut self, codepage: Codepage) -> Self {
316 self.codepage = codepage;
317 self
318 }
319
320 #[must_use]
322 #[inline]
323 pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
324 self.json_number_mode = mode;
325 self
326 }
327
328 #[must_use]
330 #[inline]
331 pub fn with_emit_filler(mut self, emit_filler: bool) -> Self {
332 self.emit_filler = emit_filler;
333 self
334 }
335
336 #[must_use]
338 #[inline]
339 pub fn with_emit_meta(mut self, emit_meta: bool) -> Self {
340 self.emit_meta = emit_meta;
341 self
342 }
343
344 #[must_use]
352 #[inline]
353 pub fn with_emit_raw(mut self, emit_raw: RawMode) -> Self {
354 self.emit_raw = emit_raw;
355 self
356 }
357
358 #[must_use]
360 #[inline]
361 pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
362 self.strict_mode = strict_mode;
363 self
364 }
365
366 #[must_use]
368 #[inline]
369 pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
370 self.max_errors = max_errors;
371 self
372 }
373
374 #[must_use]
376 #[inline]
377 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
378 self.on_decode_unmappable = policy;
379 self
380 }
381
382 #[must_use]
384 #[inline]
385 pub fn with_threads(mut self, threads: usize) -> Self {
386 self.threads = threads;
387 self
388 }
389
390 #[must_use]
398 #[inline]
399 pub fn with_preserve_zoned_encoding(mut self, preserve_zoned_encoding: bool) -> Self {
400 self.preserve_zoned_encoding = preserve_zoned_encoding;
401 self
402 }
403
404 #[must_use]
409 #[inline]
410 pub fn with_preferred_zoned_encoding(
411 mut self,
412 preferred_zoned_encoding: ZonedEncodingFormat,
413 ) -> Self {
414 self.preferred_zoned_encoding = preferred_zoned_encoding;
415 self
416 }
417
418 #[must_use]
420 #[inline]
421 pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
422 self.float_format = float_format;
423 self
424 }
425}
426
427impl Default for EncodeOptions {
428 fn default() -> Self {
429 Self {
430 format: RecordFormat::Fixed,
431 codepage: Codepage::CP037,
432 preferred_zoned_encoding: ZonedEncodingFormat::Auto,
433 use_raw: false,
434 bwz_encode: false,
435 strict_mode: false,
436 max_errors: None,
437 threads: 1,
438 coerce_numbers: false,
439 on_encode_unmappable: UnmappablePolicy::Error,
440 json_number_mode: JsonNumberMode::Lossless,
441 zoned_encoding_override: None,
442 float_format: FloatFormat::IeeeBigEndian,
443 }
444 }
445}
446
447impl EncodeOptions {
448 #[must_use]
475 #[inline]
476 pub fn new() -> Self {
477 Self::default()
478 }
479
480 #[must_use]
482 #[inline]
483 pub fn with_format(mut self, format: RecordFormat) -> Self {
484 self.format = format;
485 self
486 }
487
488 #[must_use]
490 #[inline]
491 pub fn with_codepage(mut self, codepage: Codepage) -> Self {
492 self.codepage = codepage;
493 self
494 }
495
496 #[must_use]
498 #[inline]
499 pub fn with_use_raw(mut self, use_raw: bool) -> Self {
500 self.use_raw = use_raw;
501 self
502 }
503
504 #[must_use]
506 #[inline]
507 pub fn with_bwz_encode(mut self, bwz_encode: bool) -> Self {
508 self.bwz_encode = bwz_encode;
509 self
510 }
511
512 #[must_use]
514 #[inline]
515 pub fn with_preferred_zoned_encoding(
516 mut self,
517 preferred_zoned_encoding: ZonedEncodingFormat,
518 ) -> Self {
519 self.preferred_zoned_encoding = preferred_zoned_encoding;
520 self
521 }
522
523 #[must_use]
525 #[inline]
526 pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
527 self.strict_mode = strict_mode;
528 self
529 }
530
531 #[must_use]
533 #[inline]
534 pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
535 self.max_errors = max_errors;
536 self
537 }
538
539 #[must_use]
541 #[inline]
542 pub fn with_threads(mut self, threads: usize) -> Self {
543 self.threads = threads;
544 self
545 }
546
547 #[must_use]
549 #[inline]
550 pub fn with_coerce_numbers(mut self, coerce_numbers: bool) -> Self {
551 self.coerce_numbers = coerce_numbers;
552 self
553 }
554
555 #[must_use]
557 #[inline]
558 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
559 self.on_encode_unmappable = policy;
560 self
561 }
562
563 #[must_use]
565 #[inline]
566 pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
567 self.json_number_mode = mode;
568 self
569 }
570
571 #[must_use]
577 #[inline]
578 pub fn with_zoned_encoding_override(
579 mut self,
580 zoned_encoding_override: Option<ZonedEncodingFormat>,
581 ) -> Self {
582 self.zoned_encoding_override = zoned_encoding_override;
583 self
584 }
585
586 #[must_use]
590 #[inline]
591 pub fn with_zoned_encoding_format(mut self, format: ZonedEncodingFormat) -> Self {
592 self.zoned_encoding_override = Some(format);
593 self
594 }
595
596 #[must_use]
598 #[inline]
599 pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
600 self.float_format = float_format;
601 self
602 }
603}
604impl fmt::Display for RecordFormat {
605 #[inline]
606 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
607 match self {
608 Self::Fixed => write!(f, "fixed"),
609 Self::RDW => write!(f, "rdw"),
610 }
611 }
612}
613
614impl fmt::Display for JsonNumberMode {
615 #[inline]
616 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
617 match self {
618 Self::Lossless => write!(f, "lossless"),
619 Self::Native => write!(f, "native"),
620 }
621 }
622}
623
624impl fmt::Display for RawMode {
625 #[inline]
626 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
627 match self {
628 Self::Off => write!(f, "off"),
629 Self::Record => write!(f, "record"),
630 Self::Field => write!(f, "field"),
631 Self::RecordRDW => write!(f, "record+rdw"),
632 }
633 }
634}
635
636impl fmt::Display for FloatFormat {
637 #[inline]
638 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
639 match self {
640 Self::IeeeBigEndian => write!(f, "ieee-be"),
641 Self::IbmHex => write!(f, "ibm-hex"),
642 }
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649
650 #[test]
651 fn test_zoned_encoding_format_is_ascii() {
652 assert!(ZonedEncodingFormat::Ascii.is_ascii());
653 assert!(!ZonedEncodingFormat::Ebcdic.is_ascii());
654 assert!(!ZonedEncodingFormat::Auto.is_ascii());
655 }
656
657 #[test]
658 fn test_zoned_encoding_format_is_ebcdic() {
659 assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
660 assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
661 assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
662 }
663
664 #[test]
665 fn test_zoned_encoding_format_is_auto() {
666 assert!(!ZonedEncodingFormat::Ascii.is_auto());
667 assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
668 assert!(ZonedEncodingFormat::Auto.is_auto());
669 }
670
671 #[test]
672 fn test_zoned_encoding_format_description() {
673 assert_eq!(
674 ZonedEncodingFormat::Ascii.description(),
675 "ASCII digit zones (0x30-0x39)"
676 );
677 assert_eq!(
678 ZonedEncodingFormat::Ebcdic.description(),
679 "EBCDIC digit zones (0xF0-0xF9)"
680 );
681 assert_eq!(
682 ZonedEncodingFormat::Auto.description(),
683 "Automatic detection based on zone nibbles"
684 );
685 }
686
687 #[test]
688 fn test_zoned_encoding_format_detect_from_byte() {
689 assert_eq!(
691 ZonedEncodingFormat::detect_from_byte(0x35),
692 Some(ZonedEncodingFormat::Ascii)
693 );
694 assert_eq!(
695 ZonedEncodingFormat::detect_from_byte(0x30),
696 Some(ZonedEncodingFormat::Ascii)
697 );
698 assert_eq!(
699 ZonedEncodingFormat::detect_from_byte(0x39),
700 Some(ZonedEncodingFormat::Ascii)
701 );
702
703 assert_eq!(
705 ZonedEncodingFormat::detect_from_byte(0xF5),
706 Some(ZonedEncodingFormat::Ebcdic)
707 );
708 assert_eq!(
709 ZonedEncodingFormat::detect_from_byte(0xF0),
710 Some(ZonedEncodingFormat::Ebcdic)
711 );
712 assert_eq!(
713 ZonedEncodingFormat::detect_from_byte(0xF9),
714 Some(ZonedEncodingFormat::Ebcdic)
715 );
716
717 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
719 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x50), None);
720 assert_eq!(
722 ZonedEncodingFormat::detect_from_byte(0xFF),
723 Some(ZonedEncodingFormat::Ebcdic)
724 );
725 }
726
727 #[test]
728 fn test_zoned_encoding_format_display() {
729 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
730 assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
731 assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
732 }
733
734 #[test]
735 fn test_decode_options_default() {
736 let options = DecodeOptions::default();
737 assert_eq!(options.format, RecordFormat::Fixed);
738 assert_eq!(options.codepage, Codepage::CP037);
739 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
740 assert!(!options.emit_filler);
741 assert!(!options.emit_meta);
742 assert_eq!(options.emit_raw, RawMode::Off);
743 assert!(!options.strict_mode);
744 assert!(options.max_errors.is_none());
745 assert_eq!(options.on_decode_unmappable, UnmappablePolicy::Error);
746 assert_eq!(options.threads, 1);
747 assert!(!options.preserve_zoned_encoding);
748 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
749 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
750 }
751
752 #[test]
753 fn test_encode_options_default() {
754 let options = EncodeOptions::default();
755 assert_eq!(options.format, RecordFormat::Fixed);
756 assert_eq!(options.codepage, Codepage::CP037);
757 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
758 assert!(!options.use_raw);
759 assert!(!options.bwz_encode);
760 assert!(!options.strict_mode);
761 assert_eq!(options.on_encode_unmappable, UnmappablePolicy::Error);
762 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
763 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
764 }
765
766 #[test]
767 fn test_record_format_display() {
768 assert_eq!(format!("{}", RecordFormat::Fixed), "fixed");
769 assert_eq!(format!("{}", RecordFormat::RDW), "rdw");
770 }
771
772 #[test]
773 fn test_codepage_display() {
774 assert_eq!(format!("{}", Codepage::CP037), "cp037");
775 assert_eq!(format!("{}", Codepage::CP273), "cp273");
776 assert_eq!(format!("{}", Codepage::CP500), "cp500");
777 assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
778 assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
779 }
780
781 #[test]
782 fn test_json_number_mode_display() {
783 assert_eq!(format!("{}", JsonNumberMode::Lossless), "lossless");
784 assert_eq!(format!("{}", JsonNumberMode::Native), "native");
785 }
786
787 #[test]
788 fn test_raw_mode_display() {
789 assert_eq!(format!("{}", RawMode::Off), "off");
790 assert_eq!(format!("{}", RawMode::Record), "record");
791 assert_eq!(format!("{}", RawMode::Field), "field");
792 assert_eq!(format!("{}", RawMode::RecordRDW), "record+rdw");
793 }
794
795 #[test]
796 fn test_unmappable_policy_display() {
797 assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
798 assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
799 assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
800 }
801
802 #[test]
803 fn test_decode_options_serialization() {
804 let options = DecodeOptions {
805 format: RecordFormat::Fixed,
806 codepage: Codepage::CP037,
807 json_number_mode: JsonNumberMode::Lossless,
808 emit_filler: true,
809 emit_meta: true,
810 emit_raw: RawMode::Record,
811 strict_mode: true,
812 max_errors: Some(100),
813 on_decode_unmappable: UnmappablePolicy::Replace,
814 threads: 4,
815 preserve_zoned_encoding: true,
816 preferred_zoned_encoding: ZonedEncodingFormat::Ebcdic,
817 float_format: FloatFormat::IbmHex,
818 };
819
820 let serialized = serde_json::to_string(&options).unwrap();
821 let deserialized: DecodeOptions = serde_json::from_str(&serialized).unwrap();
822
823 assert_eq!(deserialized.format, RecordFormat::Fixed);
824 assert_eq!(deserialized.codepage, Codepage::CP037);
825 assert!(deserialized.emit_filler);
826 assert!(deserialized.emit_meta);
827 assert_eq!(deserialized.emit_raw, RawMode::Record);
828 assert!(deserialized.strict_mode);
829 assert_eq!(deserialized.max_errors, Some(100));
830 assert_eq!(deserialized.on_decode_unmappable, UnmappablePolicy::Replace);
831 assert_eq!(deserialized.threads, 4);
832 assert!(deserialized.preserve_zoned_encoding);
833 assert_eq!(
834 deserialized.preferred_zoned_encoding,
835 ZonedEncodingFormat::Ebcdic
836 );
837 assert_eq!(deserialized.float_format, FloatFormat::IbmHex);
838 }
839
840 #[test]
841 fn test_decode_options_deserialize_missing_float_format_defaults() {
842 let options = DecodeOptions::default();
843 let mut value = serde_json::to_value(options).unwrap();
844 value.as_object_mut().unwrap().remove("float_format");
845 let deserialized: DecodeOptions = serde_json::from_value(value).unwrap();
846 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
847 }
848
849 #[test]
850 fn test_encode_options_deserialize_missing_float_format_defaults() {
851 let options = EncodeOptions::default();
852 let mut value = serde_json::to_value(options).unwrap();
853 value.as_object_mut().unwrap().remove("float_format");
854 let deserialized: EncodeOptions = serde_json::from_value(value).unwrap();
855 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
856 }
857}