Skip to main content

copybook_options/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Configuration option types shared across copybook-rs crates.
4//!
5//! This crate defines [`DecodeOptions`], [`EncodeOptions`], [`RecordFormat`],
6//! [`JsonNumberMode`], [`RawMode`], and [`FloatFormat`] — the common configuration
7//! surface used by the codec and CLI layers.
8#![allow(clippy::missing_inline_in_public_items)]
9
10// Re-export from copybook-charset for public API
11/// Codepage identifier for EBCDIC/ASCII character encoding.
12pub use copybook_charset::Codepage;
13/// Policy for handling unmappable characters during codepage conversion.
14pub use copybook_charset::UnmappablePolicy;
15/// Zoned decimal encoding format (ASCII, EBCDIC, or auto-detect).
16pub use copybook_zoned_format::ZonedEncodingFormat;
17use serde::{Deserialize, Serialize};
18use std::fmt;
19
20const DEFAULT_RECORD_FORMAT: RecordFormat = RecordFormat::Fixed;
21const DEFAULT_CODEPAGE: Codepage = Codepage::CP037;
22const DEFAULT_JSON_NUMBER_MODE: JsonNumberMode = JsonNumberMode::Lossless;
23const DEFAULT_UNMAPPABLE_POLICY: UnmappablePolicy = UnmappablePolicy::Error;
24const DEFAULT_THREAD_COUNT: usize = 1;
25const DEFAULT_ZONED_ENCODING: ZonedEncodingFormat = ZonedEncodingFormat::Auto;
26const DEFAULT_FLOAT_FORMAT: FloatFormat = FloatFormat::IeeeBigEndian;
27
28macro_rules! impl_common_option_builders {
29    () => {
30        /// Set the record format.
31        #[must_use]
32        #[inline]
33        pub fn with_format(mut self, format: RecordFormat) -> Self {
34            self.format = format;
35            self
36        }
37
38        /// Set the codepage.
39        #[must_use]
40        #[inline]
41        pub fn with_codepage(mut self, codepage: Codepage) -> Self {
42            self.codepage = codepage;
43            self
44        }
45
46        /// Enable or disable strict mode.
47        #[must_use]
48        #[inline]
49        pub fn with_strict_mode(mut self, strict_mode: bool) -> Self {
50            self.strict_mode = strict_mode;
51            self
52        }
53
54        /// Set the maximum number of errors before stopping.
55        #[must_use]
56        #[inline]
57        pub fn with_max_errors(mut self, max_errors: Option<u64>) -> Self {
58            self.max_errors = max_errors;
59            self
60        }
61
62        /// Set the number of threads for parallel processing.
63        #[must_use]
64        #[inline]
65        pub fn with_threads(mut self, threads: usize) -> Self {
66            self.threads = threads;
67            self
68        }
69
70        /// Set the JSON number mode.
71        #[must_use]
72        #[inline]
73        pub fn with_json_number_mode(mut self, mode: JsonNumberMode) -> Self {
74            self.json_number_mode = mode;
75            self
76        }
77
78        /// Set the preferred zoned decimal encoding format.
79        #[must_use]
80        #[inline]
81        pub fn with_preferred_zoned_encoding(
82            mut self,
83            preferred_zoned_encoding: ZonedEncodingFormat,
84        ) -> Self {
85            self.preferred_zoned_encoding = preferred_zoned_encoding;
86            self
87        }
88
89        /// Set floating-point representation for COMP-1/COMP-2 fields.
90        #[must_use]
91        #[inline]
92        pub fn with_float_format(mut self, float_format: FloatFormat) -> Self {
93            self.float_format = float_format;
94            self
95        }
96    };
97}
98
99/// Floating-point binary format for COMP-1/COMP-2 fields.
100///
101/// Copybooks define field usage but not the compiler's concrete floating-point
102/// representation. This option makes the decode/encode interpretation explicit.
103///
104/// # Examples
105///
106/// ```
107/// use copybook_options::FloatFormat;
108///
109/// let fmt = FloatFormat::default();
110/// assert_eq!(fmt, FloatFormat::IeeeBigEndian);
111/// assert_eq!(format!("{fmt}"), "ieee-be");
112/// ```
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum, Default)]
114pub enum FloatFormat {
115    /// IEEE-754 big-endian binary format.
116    #[default]
117    #[value(name = "ieee-be", alias = "ieee", alias = "ieee-big-endian")]
118    IeeeBigEndian,
119    /// IBM hexadecimal floating-point format.
120    #[value(name = "ibm-hex", alias = "ibm")]
121    IbmHex,
122}
123
124/// Options for decoding operations
125#[derive(Debug, Clone, Serialize, Deserialize)]
126#[allow(clippy::struct_excessive_bools)] // Many boolean options are needed for decode configuration
127pub struct DecodeOptions {
128    /// Record format
129    pub format: RecordFormat,
130    /// Character encoding
131    pub codepage: Codepage,
132    /// JSON number representation
133    pub json_number_mode: JsonNumberMode,
134    /// Whether to emit FILLER fields
135    pub emit_filler: bool,
136    /// Whether to emit metadata
137    pub emit_meta: bool,
138    /// Raw data capture mode
139    pub emit_raw: RawMode,
140    /// Error handling mode
141    pub strict_mode: bool,
142    /// Maximum errors before stopping
143    pub max_errors: Option<u64>,
144    /// Policy for unmappable characters
145    pub on_decode_unmappable: UnmappablePolicy,
146    /// Number of threads for parallel processing
147    pub threads: usize,
148    /// Enable zoned decimal encoding preservation for binary round-trip consistency
149    ///
150    /// When enabled, the decoder captures the original encoding format (ASCII vs EBCDIC)
151    /// and includes it in metadata for use during re-encoding to maintain byte-level
152    /// fidelity in encode/decode cycles.
153    pub preserve_zoned_encoding: bool,
154    /// Preferred encoding format when auto-detection is ambiguous
155    ///
156    /// Used as fallback when `ZonedEncodingFormat::Auto` cannot determine the format
157    /// from the data (e.g., all-zero fields, mixed encodings).
158    pub preferred_zoned_encoding: ZonedEncodingFormat,
159    /// Floating-point representation for COMP-1/COMP-2 fields.
160    #[serde(default)]
161    pub float_format: FloatFormat,
162}
163
164/// Options for encoding operations
165#[derive(Debug, Clone, Serialize, Deserialize)]
166#[allow(clippy::struct_excessive_bools)]
167pub struct EncodeOptions {
168    /// Record format
169    pub format: RecordFormat,
170    /// Character encoding
171    pub codepage: Codepage,
172    /// Fallback zoned decimal encoding format when no override or metadata applies
173    pub preferred_zoned_encoding: ZonedEncodingFormat,
174    /// Whether to use raw data when available
175    pub use_raw: bool,
176    /// BLANK WHEN ZERO encoding policy
177    pub bwz_encode: bool,
178    /// Error handling mode
179    pub strict_mode: bool,
180    /// Maximum errors before stopping
181    pub max_errors: Option<u64>,
182    /// Number of threads for parallel processing
183    pub threads: usize,
184    /// Whether to coerce non-string JSON numbers to strings before encoding
185    pub coerce_numbers: bool,
186    /// Policy for unmappable characters during encoding
187    pub on_encode_unmappable: UnmappablePolicy,
188    /// JSON number representation mode (used when round-tripping)
189    pub json_number_mode: JsonNumberMode,
190    /// Explicit zoned decimal encoding format override
191    ///
192    /// When specified, forces all zoned decimal fields to use this encoding format,
193    /// overriding any preserved format from decode operations. This provides the
194    /// highest precedence in the format selection hierarchy:
195    /// 1. Explicit override (this field)
196    /// 2. Preserved format from decode metadata
197    /// 3. EBCDIC default for mainframe compatibility
198    pub zoned_encoding_override: Option<ZonedEncodingFormat>,
199    /// Floating-point representation for COMP-1/COMP-2 fields.
200    #[serde(default)]
201    pub float_format: FloatFormat,
202}
203
204/// Record format specification
205///
206/// Controls whether records have a fixed byte length (LRECL) or use
207/// variable-length RDW (Record Descriptor Word) framing.
208///
209/// # Examples
210///
211/// ```
212/// use copybook_options::RecordFormat;
213///
214/// let fmt = RecordFormat::Fixed;
215/// assert!(fmt.is_fixed());
216/// assert!(!fmt.is_variable());
217/// assert_eq!(fmt.description(), "Fixed-length records");
218/// ```
219#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
220pub enum RecordFormat {
221    /// Fixed-length records
222    Fixed,
223    /// Variable-length records with RDW
224    RDW,
225}
226
227impl RecordFormat {
228    /// Check if this is a fixed-length record format
229    #[must_use]
230    pub const fn is_fixed(self) -> bool {
231        matches!(self, Self::Fixed)
232    }
233
234    /// Check if this is a variable-length record format
235    #[must_use]
236    pub const fn is_variable(self) -> bool {
237        matches!(self, Self::RDW)
238    }
239
240    /// Get a human-readable description of the format
241    #[must_use]
242    pub const fn description(self) -> &'static str {
243        match self {
244            Self::Fixed => "Fixed-length records",
245            Self::RDW => "Variable-length records with Record Descriptor Word",
246        }
247    }
248}
249
250/// JSON number representation mode
251///
252/// Controls how COBOL numeric fields are represented in JSON output.
253/// `Lossless` preserves exact decimal precision as strings; `Native` uses
254/// JSON number types where the value fits without precision loss.
255///
256/// # Examples
257///
258/// ```
259/// use copybook_options::JsonNumberMode;
260///
261/// let mode = JsonNumberMode::Lossless;
262/// assert!(mode.is_lossless());
263/// assert_eq!(mode.description(), "Lossless string representation for decimals");
264/// ```
265#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
266pub enum JsonNumberMode {
267    /// Lossless string representation for decimals
268    Lossless,
269    /// Native JSON numbers where possible
270    Native,
271}
272
273impl JsonNumberMode {
274    /// Check if this mode uses lossless string representation
275    #[must_use]
276    #[inline]
277    pub const fn is_lossless(self) -> bool {
278        matches!(self, Self::Lossless)
279    }
280
281    /// Check if this mode uses native JSON numbers
282    #[must_use]
283    #[inline]
284    pub const fn is_native(self) -> bool {
285        matches!(self, Self::Native)
286    }
287
288    /// Get a human-readable description of the mode
289    #[must_use]
290    #[inline]
291    pub const fn description(self) -> &'static str {
292        match self {
293            Self::Lossless => "Lossless string representation for decimals",
294            Self::Native => "Native JSON numbers where possible",
295        }
296    }
297}
298
299/// Raw data capture mode
300///
301/// Controls whether and how binary record data is captured in the output:
302/// - `Off` — no raw data (default, lowest overhead)
303/// - `Record` — full record payload in `__raw_b64`
304/// - `RecordRDW` — RDW header + payload in `__raw_b64`
305/// - `Field` — per-field raw bytes in `<FIELD_NAME>__raw_b64`
306///
307/// # Examples
308///
309/// ```
310/// use copybook_options::RawMode;
311///
312/// let mode = RawMode::Off;
313/// assert_eq!(mode, RawMode::Off);
314///
315/// let field_mode = RawMode::Field;
316/// assert_ne!(field_mode, RawMode::Off);
317/// ```
318#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, clap::ValueEnum)]
319pub enum RawMode {
320    /// No raw data capture
321    Off,
322    /// Capture record-level raw data
323    Record,
324    /// Capture field-level raw data
325    Field,
326    /// Capture record and RDW header
327    #[value(name = "record+rdw")]
328    RecordRDW,
329}
330
331impl Default for DecodeOptions {
332    fn default() -> Self {
333        Self {
334            format: DEFAULT_RECORD_FORMAT,
335            codepage: DEFAULT_CODEPAGE,
336            json_number_mode: DEFAULT_JSON_NUMBER_MODE,
337            emit_filler: false,
338            emit_meta: false,
339            emit_raw: RawMode::Off,
340            strict_mode: false,
341            max_errors: None,
342            on_decode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
343            threads: DEFAULT_THREAD_COUNT,
344            preserve_zoned_encoding: false,
345            preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
346            float_format: DEFAULT_FLOAT_FORMAT,
347        }
348    }
349}
350
351impl DecodeOptions {
352    /// Create new decode options with default values
353    ///
354    /// Returns options configured for:
355    /// - Fixed record format
356    /// - CP037 EBCDIC codepage
357    /// - Lossless JSON number mode
358    /// - Single-threaded processing
359    ///
360    /// Use the builder methods to customize:
361    ///
362    /// # Examples
363    ///
364    /// ```
365    /// use copybook_options::{DecodeOptions, Codepage, JsonNumberMode, RecordFormat, RawMode};
366    ///
367    /// let opts = DecodeOptions::new()
368    ///     .with_codepage(Codepage::CP037)
369    ///     .with_format(RecordFormat::Fixed)
370    ///     .with_json_number_mode(JsonNumberMode::Lossless)
371    ///     .with_emit_meta(true)
372    ///     .with_threads(4);
373    ///
374    /// assert_eq!(opts.threads, 4);
375    /// assert!(opts.emit_meta);
376    /// ```
377    #[must_use]
378    #[inline]
379    pub fn new() -> Self {
380        Self::default()
381    }
382
383    impl_common_option_builders!();
384
385    /// Enable or disable FILLER field emission
386    #[must_use]
387    #[inline]
388    pub fn with_emit_filler(mut self, emit_filler: bool) -> Self {
389        self.emit_filler = emit_filler;
390        self
391    }
392
393    /// Enable or disable metadata emission
394    #[must_use]
395    #[inline]
396    pub fn with_emit_meta(mut self, emit_meta: bool) -> Self {
397        self.emit_meta = emit_meta;
398        self
399    }
400
401    /// Set the raw data capture mode
402    ///
403    /// Controls whether and how raw binary data is included in decode output:
404    /// - `RawMode::Off` — no raw data (default)
405    /// - `RawMode::Record` — record payload in `__raw_b64`
406    /// - `RawMode::RecordRDW` — RDW header + payload in `__raw_b64`
407    /// - `RawMode::Field` — per-field raw values in `<FIELD>__raw_b64`
408    #[must_use]
409    #[inline]
410    pub fn with_emit_raw(mut self, emit_raw: RawMode) -> Self {
411        self.emit_raw = emit_raw;
412        self
413    }
414
415    /// Set the policy for unmappable characters
416    #[must_use]
417    #[inline]
418    pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
419        self.on_decode_unmappable = policy;
420        self
421    }
422
423    // === Zoned Decimal Encoding Configuration ===
424
425    /// Enable zoned decimal encoding preservation for round-trip fidelity
426    ///
427    /// When enabled, the decoder will detect and preserve the original encoding
428    /// format (ASCII vs EBCDIC) for use during subsequent encoding operations.
429    /// This ensures byte-level consistency in encode/decode cycles.
430    #[must_use]
431    #[inline]
432    pub fn with_preserve_zoned_encoding(mut self, preserve_zoned_encoding: bool) -> Self {
433        self.preserve_zoned_encoding = preserve_zoned_encoding;
434        self
435    }
436}
437
438impl Default for EncodeOptions {
439    fn default() -> Self {
440        Self {
441            format: DEFAULT_RECORD_FORMAT,
442            codepage: DEFAULT_CODEPAGE,
443            preferred_zoned_encoding: DEFAULT_ZONED_ENCODING,
444            use_raw: false,
445            bwz_encode: false,
446            strict_mode: false,
447            max_errors: None,
448            threads: DEFAULT_THREAD_COUNT,
449            coerce_numbers: false,
450            on_encode_unmappable: DEFAULT_UNMAPPABLE_POLICY,
451            json_number_mode: DEFAULT_JSON_NUMBER_MODE,
452            zoned_encoding_override: None,
453            float_format: DEFAULT_FLOAT_FORMAT,
454        }
455    }
456}
457
458impl EncodeOptions {
459    /// Create new encode options with default values
460    ///
461    /// Returns options configured for:
462    /// - Fixed record format
463    /// - CP037 EBCDIC codepage
464    /// - Single-threaded processing
465    /// - BLANK WHEN ZERO disabled
466    ///
467    /// Use the builder methods to customize:
468    ///
469    /// # Examples
470    ///
471    /// ```
472    /// use copybook_options::{EncodeOptions, Codepage, RecordFormat};
473    ///
474    /// let opts = EncodeOptions::new()
475    ///     .with_codepage(Codepage::CP037)
476    ///     .with_format(RecordFormat::Fixed)
477    ///     .with_bwz_encode(true)
478    ///     .with_coerce_numbers(true)
479    ///     .with_threads(4);
480    ///
481    /// assert_eq!(opts.threads, 4);
482    /// assert!(opts.bwz_encode);
483    /// assert!(opts.coerce_numbers);
484    /// ```
485    #[must_use]
486    #[inline]
487    pub fn new() -> Self {
488        Self::default()
489    }
490
491    impl_common_option_builders!();
492
493    /// Enable or disable raw data usage
494    #[must_use]
495    #[inline]
496    pub fn with_use_raw(mut self, use_raw: bool) -> Self {
497        self.use_raw = use_raw;
498        self
499    }
500
501    /// Enable or disable BLANK WHEN ZERO encoding
502    #[must_use]
503    #[inline]
504    pub fn with_bwz_encode(mut self, bwz_encode: bool) -> Self {
505        self.bwz_encode = bwz_encode;
506        self
507    }
508
509    /// Enable or disable number coercion
510    #[must_use]
511    #[inline]
512    pub fn with_coerce_numbers(mut self, coerce_numbers: bool) -> Self {
513        self.coerce_numbers = coerce_numbers;
514        self
515    }
516
517    /// Set the policy for unmappable characters during encoding
518    #[must_use]
519    #[inline]
520    pub fn with_unmappable_policy(mut self, policy: UnmappablePolicy) -> Self {
521        self.on_encode_unmappable = policy;
522        self
523    }
524
525    /// Set explicit zoned decimal encoding format override
526    ///
527    /// Forces all zoned decimal fields to use the specified encoding format,
528    /// overriding any preserved format from decode operations. Use `None` to
529    /// disable override and respect preserved formats.
530    #[must_use]
531    #[inline]
532    pub fn with_zoned_encoding_override(
533        mut self,
534        zoned_encoding_override: Option<ZonedEncodingFormat>,
535    ) -> Self {
536        self.zoned_encoding_override = zoned_encoding_override;
537        self
538    }
539
540    /// Convenience method to set explicit zoned encoding format
541    ///
542    /// Equivalent to `with_zoned_encoding_override(Some(format))`.
543    #[must_use]
544    #[inline]
545    pub fn with_zoned_encoding_format(mut self, format: ZonedEncodingFormat) -> Self {
546        self.zoned_encoding_override = Some(format);
547        self
548    }
549}
550impl fmt::Display for RecordFormat {
551    #[inline]
552    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
553        match self {
554            Self::Fixed => write!(f, "fixed"),
555            Self::RDW => write!(f, "rdw"),
556        }
557    }
558}
559
560impl fmt::Display for JsonNumberMode {
561    #[inline]
562    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563        match self {
564            Self::Lossless => write!(f, "lossless"),
565            Self::Native => write!(f, "native"),
566        }
567    }
568}
569
570impl fmt::Display for RawMode {
571    #[inline]
572    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
573        match self {
574            Self::Off => write!(f, "off"),
575            Self::Record => write!(f, "record"),
576            Self::Field => write!(f, "field"),
577            Self::RecordRDW => write!(f, "record+rdw"),
578        }
579    }
580}
581
582impl fmt::Display for FloatFormat {
583    #[inline]
584    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
585        match self {
586            Self::IeeeBigEndian => write!(f, "ieee-be"),
587            Self::IbmHex => write!(f, "ibm-hex"),
588        }
589    }
590}
591
592#[cfg(test)]
593mod tests {
594    use super::*;
595
596    #[test]
597    fn test_zoned_encoding_format_is_ascii() {
598        assert!(ZonedEncodingFormat::Ascii.is_ascii());
599        assert!(!ZonedEncodingFormat::Ebcdic.is_ascii());
600        assert!(!ZonedEncodingFormat::Auto.is_ascii());
601    }
602
603    #[test]
604    fn test_zoned_encoding_format_is_ebcdic() {
605        assert!(!ZonedEncodingFormat::Ascii.is_ebcdic());
606        assert!(ZonedEncodingFormat::Ebcdic.is_ebcdic());
607        assert!(!ZonedEncodingFormat::Auto.is_ebcdic());
608    }
609
610    #[test]
611    fn test_zoned_encoding_format_is_auto() {
612        assert!(!ZonedEncodingFormat::Ascii.is_auto());
613        assert!(!ZonedEncodingFormat::Ebcdic.is_auto());
614        assert!(ZonedEncodingFormat::Auto.is_auto());
615    }
616
617    #[test]
618    fn test_zoned_encoding_format_description() {
619        assert_eq!(
620            ZonedEncodingFormat::Ascii.description(),
621            "ASCII digit zones (0x30-0x39)"
622        );
623        assert_eq!(
624            ZonedEncodingFormat::Ebcdic.description(),
625            "EBCDIC digit zones (0xF0-0xF9)"
626        );
627        assert_eq!(
628            ZonedEncodingFormat::Auto.description(),
629            "Automatic detection based on zone nibbles"
630        );
631    }
632
633    #[test]
634    fn test_zoned_encoding_format_detect_from_byte() {
635        // ASCII zone nibble (0x30)
636        assert_eq!(
637            ZonedEncodingFormat::detect_from_byte(0x35),
638            Some(ZonedEncodingFormat::Ascii)
639        );
640        assert_eq!(
641            ZonedEncodingFormat::detect_from_byte(0x30),
642            Some(ZonedEncodingFormat::Ascii)
643        );
644        assert_eq!(
645            ZonedEncodingFormat::detect_from_byte(0x39),
646            Some(ZonedEncodingFormat::Ascii)
647        );
648
649        // EBCDIC zone nibble (0xF0)
650        assert_eq!(
651            ZonedEncodingFormat::detect_from_byte(0xF5),
652            Some(ZonedEncodingFormat::Ebcdic)
653        );
654        assert_eq!(
655            ZonedEncodingFormat::detect_from_byte(0xF0),
656            Some(ZonedEncodingFormat::Ebcdic)
657        );
658        assert_eq!(
659            ZonedEncodingFormat::detect_from_byte(0xF9),
660            Some(ZonedEncodingFormat::Ebcdic)
661        );
662
663        // Invalid zone nibbles (0x00, 0x50)
664        assert_eq!(ZonedEncodingFormat::detect_from_byte(0x00), None);
665        assert_eq!(ZonedEncodingFormat::detect_from_byte(0x50), None);
666        // Note: 0xFF matches EBCDIC_ZONE (0x0F), so it returns Some(Ebcdic)
667        assert_eq!(
668            ZonedEncodingFormat::detect_from_byte(0xFF),
669            Some(ZonedEncodingFormat::Ebcdic)
670        );
671    }
672
673    #[test]
674    fn test_zoned_encoding_format_display() {
675        assert_eq!(format!("{}", ZonedEncodingFormat::Ascii), "ascii");
676        assert_eq!(format!("{}", ZonedEncodingFormat::Ebcdic), "ebcdic");
677        assert_eq!(format!("{}", ZonedEncodingFormat::Auto), "auto");
678    }
679
680    #[test]
681    fn test_decode_options_default() {
682        let options = DecodeOptions::default();
683        assert_eq!(options.format, RecordFormat::Fixed);
684        assert_eq!(options.codepage, Codepage::CP037);
685        assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
686        assert!(!options.emit_filler);
687        assert!(!options.emit_meta);
688        assert_eq!(options.emit_raw, RawMode::Off);
689        assert!(!options.strict_mode);
690        assert!(options.max_errors.is_none());
691        assert_eq!(options.on_decode_unmappable, UnmappablePolicy::Error);
692        assert_eq!(options.threads, 1);
693        assert!(!options.preserve_zoned_encoding);
694        assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
695        assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
696    }
697
698    #[test]
699    fn test_encode_options_default() {
700        let options = EncodeOptions::default();
701        assert_eq!(options.format, RecordFormat::Fixed);
702        assert_eq!(options.codepage, Codepage::CP037);
703        assert_eq!(options.preferred_zoned_encoding, ZonedEncodingFormat::Auto);
704        assert!(!options.use_raw);
705        assert!(!options.bwz_encode);
706        assert!(!options.strict_mode);
707        assert_eq!(options.on_encode_unmappable, UnmappablePolicy::Error);
708        assert_eq!(options.json_number_mode, JsonNumberMode::Lossless);
709        assert_eq!(options.float_format, FloatFormat::IeeeBigEndian);
710    }
711
712    #[test]
713    fn test_record_format_display() {
714        assert_eq!(format!("{}", RecordFormat::Fixed), "fixed");
715        assert_eq!(format!("{}", RecordFormat::RDW), "rdw");
716    }
717
718    #[test]
719    fn test_codepage_display() {
720        assert_eq!(format!("{}", Codepage::CP037), "cp037");
721        assert_eq!(format!("{}", Codepage::CP273), "cp273");
722        assert_eq!(format!("{}", Codepage::CP500), "cp500");
723        assert_eq!(format!("{}", Codepage::CP1047), "cp1047");
724        assert_eq!(format!("{}", Codepage::CP1140), "cp1140");
725    }
726
727    #[test]
728    fn test_json_number_mode_display() {
729        assert_eq!(format!("{}", JsonNumberMode::Lossless), "lossless");
730        assert_eq!(format!("{}", JsonNumberMode::Native), "native");
731    }
732
733    #[test]
734    fn test_raw_mode_display() {
735        assert_eq!(format!("{}", RawMode::Off), "off");
736        assert_eq!(format!("{}", RawMode::Record), "record");
737        assert_eq!(format!("{}", RawMode::Field), "field");
738        assert_eq!(format!("{}", RawMode::RecordRDW), "record+rdw");
739    }
740
741    #[test]
742    fn test_unmappable_policy_display() {
743        assert_eq!(format!("{}", UnmappablePolicy::Error), "error");
744        assert_eq!(format!("{}", UnmappablePolicy::Replace), "replace");
745        assert_eq!(format!("{}", UnmappablePolicy::Skip), "skip");
746    }
747
748    #[test]
749    fn test_decode_options_serialization() {
750        let options = DecodeOptions {
751            format: DEFAULT_RECORD_FORMAT,
752            codepage: DEFAULT_CODEPAGE,
753            json_number_mode: DEFAULT_JSON_NUMBER_MODE,
754            emit_filler: true,
755            emit_meta: true,
756            emit_raw: RawMode::Record,
757            strict_mode: true,
758            max_errors: Some(100),
759            on_decode_unmappable: UnmappablePolicy::Replace,
760            threads: 4,
761            preserve_zoned_encoding: true,
762            preferred_zoned_encoding: ZonedEncodingFormat::Ebcdic,
763            float_format: FloatFormat::IbmHex,
764        };
765
766        let serialized = serde_json::to_string(&options).unwrap();
767        let deserialized: DecodeOptions = serde_json::from_str(&serialized).unwrap();
768
769        assert_eq!(deserialized.format, RecordFormat::Fixed);
770        assert_eq!(deserialized.codepage, Codepage::CP037);
771        assert!(deserialized.emit_filler);
772        assert!(deserialized.emit_meta);
773        assert_eq!(deserialized.emit_raw, RawMode::Record);
774        assert!(deserialized.strict_mode);
775        assert_eq!(deserialized.max_errors, Some(100));
776        assert_eq!(deserialized.on_decode_unmappable, UnmappablePolicy::Replace);
777        assert_eq!(deserialized.threads, 4);
778        assert!(deserialized.preserve_zoned_encoding);
779        assert_eq!(
780            deserialized.preferred_zoned_encoding,
781            ZonedEncodingFormat::Ebcdic
782        );
783        assert_eq!(deserialized.float_format, FloatFormat::IbmHex);
784    }
785
786    #[test]
787    fn test_decode_options_deserialize_missing_float_format_defaults() {
788        let options = DecodeOptions::default();
789        let mut value = serde_json::to_value(options).unwrap();
790        value.as_object_mut().unwrap().remove("float_format");
791        let deserialized: DecodeOptions = serde_json::from_value(value).unwrap();
792        assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
793    }
794
795    #[test]
796    fn test_encode_options_deserialize_missing_float_format_defaults() {
797        let options = EncodeOptions::default();
798        let mut value = serde_json::to_value(options).unwrap();
799        value.as_object_mut().unwrap().remove("float_format");
800        let deserialized: EncodeOptions = serde_json::from_value(value).unwrap();
801        assert_eq!(deserialized.float_format, FloatFormat::IeeeBigEndian);
802    }
803}