1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2#![allow(clippy::missing_inline_in_public_items)]
10
11mod parse_error;
12pub use parse_error::{CodecOptionKind, ParseCodecOptionError};
13
14pub use crate::numeric::ZonedEncodingFormat;
17pub use copybook_charset::Codepage;
19pub use copybook_charset::UnmappablePolicy;
21use serde::{Deserialize, Serialize};
22use std::{fmt, str::FromStr};
23
24const DEFAULT_RECORD_FORMAT: RecordFormat = RecordFormat::Fixed;
25const DEFAULT_CODEPAGE: Codepage = Codepage::CP037;
26const DEFAULT_JSON_NUMBER_MODE: JsonNumberMode = JsonNumberMode::Lossless;
27const DEFAULT_UNMAPPABLE_POLICY: UnmappablePolicy = UnmappablePolicy::Error;
28const DEFAULT_THREAD_COUNT: usize = 1;
29const DEFAULT_ZONED_ENCODING: ZonedEncodingFormat = ZonedEncodingFormat::Auto;
30const DEFAULT_FLOAT_FORMAT: FloatFormat = FloatFormat::IeeeBigEndian;
31
32macro_rules! impl_common_option_builders {
33 () => {
34 #[must_use]
36 #[inline]
37 pub fn with_format(mut self, format: RecordFormat) -> Self {
38 self.format = format;
39 self
40 }
41
42 #[must_use]
44 #[inline]
45 pub fn with_codepage(mut self, codepage: Codepage) -> Self {
46 self.codepage = codepage;
47 self
48 }
49
50 #[must_use]
52 #[inline]
53 pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
54 self.strict_mode = strict_mode;
55 self
56 }
57
58 #[must_use]
60 #[inline]
61 pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
62 self.max_errors = max_errors;
63 self
64 }
65
66 #[must_use]
68 #[inline]
69 pub fn with_threads(mut self, threads: usize) -> Self {
70 self.threads = threads;
71 self
72 }
73
74 #[must_use]
76 #[inline]
77 pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
78 self.json_number_mode = mode;
79 self
80 }
81
82 #[must_use]
84 #[inline]
85 pub fn with_preferred_zoned_encoding(
86 mut self,
87 preferred_zoned_encoding: ZonedEncodingFormat,
88 ) -> Self {
89 self.preferred_zoned_encoding = preferred_zoned_encoding;
90 self
91 }
92
93 #[must_use]
95 #[inline]
96 pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
97 self.float_format = float_format;
98 self
99 }
100 };
101}
102
103#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
118pub enum FloatFormat {
119 #[default]
121 IeeeBigEndian,
122 IbmHex,
124}
125
126impl FromStr for FloatFormat {
127 type Err = ParseCodecOptionError;
128
129 fn from_str(input: &str) -> Result<Self, Self::Err> {
130 match input.to_ascii_lowercase().as_str() {
131 "ieee-be" | "ieee" | "ieee-big-endian" => Ok(Self::IeeeBigEndian),
132 "ibm-hex" | "ibm" => Ok(Self::IbmHex),
133 _ => Err(ParseCodecOptionError::new(
134 CodecOptionKind::FloatFormat,
135 input,
136 )),
137 }
138 }
139}
140
141#[derive(Debug, Clone, Serialize, Deserialize)]
143#[allow(clippy::struct_excessive_bools)] pub struct DecodeOptions {
145 pub format: RecordFormat,
147 pub codepage: Codepage,
149 pub json_number_mode: JsonNumberMode,
151 pub emit_filler: bool,
153 pub emit_meta: bool,
155 pub emit_raw: RawMode,
157 pub strict_mode: bool,
159 pub max_errors: Option<u64>,
161 pub on_decode_unmappable: UnmappablePolicy,
163 pub threads: usize,
165 pub preserve_zoned_encoding: bool,
171 pub preferred_zoned_encoding: ZonedEncodingFormat,
176 #[serde(default)]
178 pub float_format: FloatFormat,
179}
180
181#[derive(Debug, Clone, Serialize, Deserialize)]
183#[allow(clippy::struct_excessive_bools)]
184pub struct EncodeOptions {
185 pub format: RecordFormat,
187 pub codepage: Codepage,
189 pub preferred_zoned_encoding: ZonedEncodingFormat,
191 pub use_raw: bool,
193 pub bwz_encode: bool,
195 pub strict_mode: bool,
197 pub max_errors: Option<u64>,
199 pub threads: usize,
201 pub coerce_numbers: bool,
203 pub on_encode_unmappable: UnmappablePolicy,
205 pub json_number_mode: JsonNumberMode,
207 pub zoned_encoding_override: Option<ZonedEncodingFormat>,
216 #[serde(default)]
218 pub float_format: FloatFormat,
219}
220
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
237pub enum RecordFormat {
238 Fixed,
240 RDW,
242}
243
244impl FromStr for RecordFormat {
245 type Err = ParseCodecOptionError;
246
247 fn from_str(input: &str) -> Result<Self, Self::Err> {
248 match input.to_ascii_lowercase().as_str() {
249 "fixed" => Ok(Self::Fixed),
250 "rdw" => Ok(Self::RDW),
251 _ => Err(ParseCodecOptionError::new(
252 CodecOptionKind::RecordFormat,
253 input,
254 )),
255 }
256 }
257}
258
259impl RecordFormat {
260 #[must_use]
262 pub const fn is_fixed(self) -> bool {
263 matches!(self, Self::Fixed)
264 }
265
266 #[must_use]
268 pub const fn is_variable(self) -> bool {
269 matches!(self, Self::RDW)
270 }
271
272 #[must_use]
274 pub const fn description(self) -> &'static str {
275 match self {
276 Self::Fixed => "Fixed-length records",
277 Self::RDW => "Variable-length records with Record Descriptor Word",
278 }
279 }
280}
281
282#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
298pub enum JsonNumberMode {
299 Lossless,
301 Native,
303}
304
305impl FromStr for JsonNumberMode {
306 type Err = ParseCodecOptionError;
307
308 fn from_str(input: &str) -> Result<Self, Self::Err> {
309 match input.to_ascii_lowercase().as_str() {
310 "lossless" => Ok(Self::Lossless),
311 "native" => Ok(Self::Native),
312 _ => Err(ParseCodecOptionError::new(
313 CodecOptionKind::JsonNumberMode,
314 input,
315 )),
316 }
317 }
318}
319
320impl JsonNumberMode {
321 #[must_use]
323 #[inline]
324 pub const fn is_lossless(self) -> bool {
325 matches!(self, Self::Lossless)
326 }
327
328 #[must_use]
330 #[inline]
331 pub const fn is_native(self) -> bool {
332 matches!(self, Self::Native)
333 }
334
335 #[must_use]
337 #[inline]
338 pub const fn description(self) -> &'static str {
339 match self {
340 Self::Lossless => "Lossless string representation for decimals",
341 Self::Native => "Native JSON numbers where possible",
342 }
343 }
344}
345
346#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
369pub enum RawMode {
370 Off,
372 Record,
374 Field,
376 RecordRDW,
378}
379
380impl FromStr for RawMode {
381 type Err = ParseCodecOptionError;
382
383 fn from_str(input: &str) -> Result<Self, Self::Err> {
384 match input.to_ascii_lowercase().as_str() {
385 "off" => Ok(Self::Off),
386 "record" => Ok(Self::Record),
387 "field" => Ok(Self::Field),
388 "record+rdw" => Ok(Self::RecordRDW),
389 _ => Err(ParseCodecOptionError::new(CodecOptionKind::RawMode, input)),
390 }
391 }
392}
393
394impl Default for DecodeOptions {
395 fn default() -> Self {
396 Self {
397 format: DEFAULT_RECORD_FORMAT,
398 codepage: DEFAULT_CODEPAGE,
399 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
400 emit_filler: false,
401 emit_meta: false,
402 emit_raw: RawMode::Off,
403 strict_mode: false,
404 max_errors: None,
405 on_decode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
406 threads: DEFAULT_THREAD_COUNT,
407 preserve_zoned_encoding: false,
408 preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
409 float_format: DEFAULT_FLOAT_FORMAT,
410 }
411 }
412}
413
414impl DecodeOptions {
415 #[must_use]
441 #[inline]
442 pub fn new() -> Self {
443 Self::default()
444 }
445
446 impl_common_option_builders!();
447
448 #[must_use]
450 #[inline]
451 pub fn with_emit_filler(mut self, emit_filler: bool) -> Self {
452 self.emit_filler = emit_filler;
453 self
454 }
455
456 #[must_use]
458 #[inline]
459 pub fn with_emit_meta(mut self, emit_meta: bool) -> Self {
460 self.emit_meta = emit_meta;
461 self
462 }
463
464 #[must_use]
474 #[inline]
475 pub fn with_emit_raw(mut self, emit_raw: RawMode) -> Self {
476 self.emit_raw = emit_raw;
477 self
478 }
479
480 #[must_use]
482 #[inline]
483 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
484 self.on_decode_unmappable = policy;
485 self
486 }
487
488 #[must_use]
496 #[inline]
497 pub fn with_preserve_zoned_encoding(mut self, preserve_zoned_encoding: bool) -> Self {
498 self.preserve_zoned_encoding = preserve_zoned_encoding;
499 self
500 }
501}
502
503impl Default for EncodeOptions {
504 fn default() -> Self {
505 Self {
506 format: DEFAULT_RECORD_FORMAT,
507 codepage: DEFAULT_CODEPAGE,
508 preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
509 use_raw: false,
510 bwz_encode: false,
511 strict_mode: false,
512 max_errors: None,
513 threads: DEFAULT_THREAD_COUNT,
514 coerce_numbers: false,
515 on_encode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
516 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
517 zoned_encoding_override: None,
518 float_format: DEFAULT_FLOAT_FORMAT,
519 }
520 }
521}
522
523impl EncodeOptions {
524 #[must_use]
551 #[inline]
552 pub fn new() -> Self {
553 Self::default()
554 }
555
556 impl_common_option_builders!();
557
558 #[must_use]
560 #[inline]
561 pub fn with_use_raw(mut self, use_raw: bool) -> Self {
562 self.use_raw = use_raw;
563 self
564 }
565
566 #[must_use]
568 #[inline]
569 pub fn with_bwz_encode(mut self, bwz_encode: bool) -> Self {
570 self.bwz_encode = bwz_encode;
571 self
572 }
573
574 #[must_use]
576 #[inline]
577 pub fn with_coerce_numbers(mut self, coerce_numbers: bool) -> Self {
578 self.coerce_numbers = coerce_numbers;
579 self
580 }
581
582 #[must_use]
584 #[inline]
585 pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
586 self.on_encode_unmappable = policy;
587 self
588 }
589
590 #[must_use]
596 #[inline]
597 pub fn with_zoned_encoding_override(
598 mut self,
599 zoned_encoding_override: Option<ZonedEncodingFormat>,
600 ) -> Self {
601 self.zoned_encoding_override = zoned_encoding_override;
602 self
603 }
604
605 #[must_use]
609 #[inline]
610 pub fn with_zoned_encoding_format(mut self, format: ZonedEncodingFormat) -> Self {
611 self.zoned_encoding_override = Some(format);
612 self
613 }
614}
615impl fmt::Display for RecordFormat {
616 #[inline]
617 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
618 match self {
619 Self::Fixed => write!(f, "fixed"),
620 Self::RDW => write!(f, "rdw"),
621 }
622 }
623}
624
625impl fmt::Display for JsonNumberMode {
626 #[inline]
627 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
628 match self {
629 Self::Lossless => write!(f, "lossless"),
630 Self::Native => write!(f, "native"),
631 }
632 }
633}
634
635impl fmt::Display for RawMode {
636 #[inline]
637 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
638 match self {
639 Self::Off => write!(f, "off"),
640 Self::Record => write!(f, "record"),
641 Self::Field => write!(f, "field"),
642 Self::RecordRDW => write!(f, "record+rdw"),
643 }
644 }
645}
646
647impl fmt::Display for FloatFormat {
648 #[inline]
649 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
650 match self {
651 Self::IeeeBigEndian => write!(f, "ieee-be"),
652 Self::IbmHex => write!(f, "ibm-hex"),
653 }
654 }
655}
656
657#[cfg(test)]
658mod tests {
659 use super::*;
660
661 #[test]
662 fn test_zoned_encoding_format_is_ascii() {
663 assert!(ZonedEncodingFormat::Ascii.is_ascii());
664 assert!(!ZonedEncodingFormat::Ebcdic.is_ascii());
665 assert!(!ZonedEncodingFormat::Auto.is_ascii());
666 }
667
668 #[test]
669 fn test_zoned_encoding_format_is_ebcdic() {
670 assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
671 assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
672 assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
673 }
674
675 #[test]
676 fn test_zoned_encoding_format_is_auto() {
677 assert!(!ZonedEncodingFormat::Ascii.is_auto());
678 assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
679 assert!(ZonedEncodingFormat::Auto.is_auto());
680 }
681
682 #[test]
683 fn test_zoned_encoding_format_description() {
684 assert_eq!(
685 ZonedEncodingFormat::Ascii.description(),
686 "ASCII digit zones (0x30-0x39)"
687 );
688 assert_eq!(
689 ZonedEncodingFormat::Ebcdic.description(),
690 "EBCDIC digit zones (0xF0-0xF9)"
691 );
692 assert_eq!(
693 ZonedEncodingFormat::Auto.description(),
694 "Automatic detection based on zone nibbles"
695 );
696 }
697
698 #[test]
699 fn test_zoned_encoding_format_detect_from_byte() {
700 assert_eq!(
702 ZonedEncodingFormat::detect_from_byte(0x35),
703 Some(ZonedEncodingFormat::Ascii)
704 );
705 assert_eq!(
706 ZonedEncodingFormat::detect_from_byte(0x30),
707 Some(ZonedEncodingFormat::Ascii)
708 );
709 assert_eq!(
710 ZonedEncodingFormat::detect_from_byte(0x39),
711 Some(ZonedEncodingFormat::Ascii)
712 );
713
714 assert_eq!(
716 ZonedEncodingFormat::detect_from_byte(0xF5),
717 Some(ZonedEncodingFormat::Ebcdic)
718 );
719 assert_eq!(
720 ZonedEncodingFormat::detect_from_byte(0xF0),
721 Some(ZonedEncodingFormat::Ebcdic)
722 );
723 assert_eq!(
724 ZonedEncodingFormat::detect_from_byte(0xF9),
725 Some(ZonedEncodingFormat::Ebcdic)
726 );
727
728 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
730 assert_eq!(ZonedEncodingFormat::detect_from_byte(0x50), None);
731 assert_eq!(
733 ZonedEncodingFormat::detect_from_byte(0xFF),
734 Some(ZonedEncodingFormat::Ebcdic)
735 );
736 }
737
738 #[test]
739 fn test_zoned_encoding_format_display() {
740 assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
741 assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
742 assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
743 }
744
745 #[test]
746 fn test_decode_options_default() {
747 let options = DecodeOptions::default();
748 assert_eq!(options.format, RecordFormat::Fixed);
749 assert_eq!(options.codepage, Codepage::CP037);
750 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
751 assert!(!options.emit_filler);
752 assert!(!options.emit_meta);
753 assert_eq!(options.emit_raw, RawMode::Off);
754 assert!(!options.strict_mode);
755 assert!(options.max_errors.is_none());
756 assert_eq!(options.on_decode_unmappable, UnmappablePolicy::Error);
757 assert_eq!(options.threads, 1);
758 assert!(!options.preserve_zoned_encoding);
759 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
760 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
761 }
762
763 #[test]
764 fn test_encode_options_default() {
765 let options = EncodeOptions::default();
766 assert_eq!(options.format, RecordFormat::Fixed);
767 assert_eq!(options.codepage, Codepage::CP037);
768 assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
769 assert!(!options.use_raw);
770 assert!(!options.bwz_encode);
771 assert!(!options.strict_mode);
772 assert_eq!(options.on_encode_unmappable, UnmappablePolicy::Error);
773 assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
774 assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
775 }
776
777 #[test]
778 fn test_record_format_display() {
779 assert_eq!(format!("{}", RecordFormat::Fixed), "fixed");
780 assert_eq!(format!("{}", RecordFormat::RDW), "rdw");
781 }
782
783 #[test]
784 fn test_codepage_display() {
785 assert_eq!(format!("{}", Codepage::CP037), "cp037");
786 assert_eq!(format!("{}", Codepage::CP273), "cp273");
787 assert_eq!(format!("{}", Codepage::CP500), "cp500");
788 assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
789 assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
790 }
791
792 #[test]
793 fn test_json_number_mode_display() {
794 assert_eq!(format!("{}", JsonNumberMode::Lossless), "lossless");
795 assert_eq!(format!("{}", JsonNumberMode::Native), "native");
796 }
797
798 #[test]
799 fn test_raw_mode_display() {
800 assert_eq!(format!("{}", RawMode::Off), "off");
801 assert_eq!(format!("{}", RawMode::Record), "record");
802 assert_eq!(format!("{}", RawMode::Field), "field");
803 assert_eq!(format!("{}", RawMode::RecordRDW), "record+rdw");
804 }
805
806 #[test]
807 fn test_unmappable_policy_display() {
808 assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
809 assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
810 assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
811 }
812
813 #[test]
814 fn test_decode_options_serialization() {
815 let options = DecodeOptions {
816 format: DEFAULT_RECORD_FORMAT,
817 codepage: DEFAULT_CODEPAGE,
818 json_number_mode: DEFAULT_JSON_NUMBER_MODE,
819 emit_filler: true,
820 emit_meta: true,
821 emit_raw: RawMode::Record,
822 strict_mode: true,
823 max_errors: Some(100),
824 on_decode_unmappable: UnmappablePolicy::Replace,
825 threads: 4,
826 preserve_zoned_encoding: true,
827 preferred_zoned_encoding: ZonedEncodingFormat::Ebcdic,
828 float_format: FloatFormat::IbmHex,
829 };
830
831 let serialized = serde_json::to_string(&options).unwrap();
832 let deserialized: DecodeOptions = serde_json::from_str(&serialized).unwrap();
833
834 assert_eq!(deserialized.format, RecordFormat::Fixed);
835 assert_eq!(deserialized.codepage, Codepage::CP037);
836 assert!(deserialized.emit_filler);
837 assert!(deserialized.emit_meta);
838 assert_eq!(deserialized.emit_raw, RawMode::Record);
839 assert!(deserialized.strict_mode);
840 assert_eq!(deserialized.max_errors, Some(100));
841 assert_eq!(deserialized.on_decode_unmappable, UnmappablePolicy::Replace);
842 assert_eq!(deserialized.threads, 4);
843 assert!(deserialized.preserve_zoned_encoding);
844 assert_eq!(
845 deserialized.preferred_zoned_encoding,
846 ZonedEncodingFormat::Ebcdic
847 );
848 assert_eq!(deserialized.float_format, FloatFormat::IbmHex);
849 }
850
851 #[test]
852 fn test_decode_options_deserialize_missing_float_format_defaults() {
853 let options = DecodeOptions::default();
854 let mut value = serde_json::to_value(options).unwrap();
855 value.as_object_mut().unwrap().remove("float_format");
856 let deserialized: DecodeOptions = serde_json::from_value(value).unwrap();
857 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
858 }
859
860 #[test]
861 fn test_encode_options_deserialize_missing_float_format_defaults() {
862 let options = EncodeOptions::default();
863 let mut value = serde_json::to_value(options).unwrap();
864 value.as_object_mut().unwrap().remove("float_format");
865 let deserialized: EncodeOptions = serde_json::from_value(value).unwrap();
866 assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
867 }
868}