Skip to main content

copybook_record_io/
lib.rs

1#![cfg_attr(not(test), deny(clippy::unwrap_used, clippy::expect_used))]
2// SPDX-License-Identifier: AGPL-3.0-or-later
3//! Record-format dispatch and facade re-exports for framing microcrates.
4//!
5//! This crate deliberately owns one concern: route legacy single-record I/O
6//! calls to either fixed-LRECL or RDW framing implementations.
7//!
8//! Use [`read_record`] and [`write_record`] for format-agnostic single-record
9//! I/O, or import the framing types directly for streaming access.
10
11use copybook_error::{Error, ErrorCode, Result};
12use copybook_options::RecordFormat;
13use std::io::{Read, Write};
14
15/// Re-export fixed-length record reader from [`copybook_fixed`].
16pub use copybook_fixed::FixedRecordReader;
17/// Re-export fixed-length record writer from [`copybook_fixed`].
18pub use copybook_fixed::FixedRecordWriter;
19/// Re-export RDW record type from [`copybook_rdw`].
20pub use copybook_rdw::RDWRecord;
21/// Re-export RDW record reader from [`copybook_rdw`].
22pub use copybook_rdw::RDWRecordReader;
23/// Re-export RDW record writer from [`copybook_rdw`].
24pub use copybook_rdw::RDWRecordWriter;
25
26/// Read one record from input using the selected record format.
27///
28/// # Errors
29/// Returns an error when the delegated fixed/RDW framing read fails.
30#[inline]
31#[must_use = "Handle the Result or propagate the error"]
32pub fn read_record(
33    input: &mut impl Read,
34    format: RecordFormat,
35    lrecl: Option<u32>,
36) -> Result<Option<Vec<u8>>> {
37    match format {
38        RecordFormat::Fixed => read_fixed_record(input, lrecl),
39        RecordFormat::RDW => read_rdw_record(input),
40    }
41}
42
43#[inline]
44fn read_fixed_record(input: &mut impl Read, lrecl: Option<u32>) -> Result<Option<Vec<u8>>> {
45    let mut reader = FixedRecordReader::new(input, lrecl)?;
46    reader.read_record()
47}
48
49#[inline]
50fn read_rdw_record(input: &mut impl Read) -> Result<Option<Vec<u8>>> {
51    let mut reader = RDWRecordReader::new(input, false);
52    match reader.read_record()? {
53        Some(record) => Ok(Some(record.payload)),
54        None => Ok(None),
55    }
56}
57
58/// Write one record to output using the selected record format.
59///
60/// # Errors
61/// Returns an error when the delegated fixed/RDW framing write fails.
62#[inline]
63#[must_use = "Handle the Result or propagate the error"]
64pub fn write_record(output: &mut impl Write, data: &[u8], format: RecordFormat) -> Result<()> {
65    match format {
66        RecordFormat::Fixed => {
67            output.write_all(data).map_err(|e| {
68                Error::new(
69                    ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
70                    format!("Write error: {e}"),
71                )
72            })?;
73            Ok(())
74        }
75        RecordFormat::RDW => {
76            let mut writer = RDWRecordWriter::new(output);
77            writer.write_record_from_payload(data, None)
78        }
79    }
80}
81
82#[cfg(test)]
83#[allow(clippy::expect_used, clippy::unwrap_used, clippy::panic)]
84mod tests {
85    use super::*;
86    use proptest::collection::vec;
87    use proptest::prelude::*;
88    use std::io::Cursor;
89
90    #[test]
91    fn read_fixed_record_delegates_to_fixed_microcrate() {
92        let mut cursor = Cursor::new(b"LOCKTEST".to_vec());
93        let record = read_record(&mut cursor, RecordFormat::Fixed, Some(8))
94            .unwrap()
95            .unwrap();
96        assert_eq!(record, b"LOCKTEST");
97    }
98
99    #[test]
100    fn read_record_fixed_requires_lrecl() {
101        let mut cursor = Cursor::new(b"LOCKTEST".to_vec());
102        let err = read_record(&mut cursor, RecordFormat::Fixed, None).unwrap_err();
103        assert_eq!(err.code, ErrorCode::CBKI001_INVALID_STATE);
104    }
105
106    #[test]
107    fn read_rdw_record_returns_payload_only() {
108        let mut cursor = Cursor::new(vec![0x00, 0x04, 0x00, 0x00, b'T', b'E', b'S', b'T']);
109        let record = read_record(&mut cursor, RecordFormat::RDW, None)
110            .unwrap()
111            .unwrap();
112        assert_eq!(record, b"TEST");
113    }
114
115    #[test]
116    fn write_fixed_record_passthrough() {
117        let mut output = Vec::new();
118        write_record(&mut output, b"LOCK", RecordFormat::Fixed).unwrap();
119        assert_eq!(output, b"LOCK");
120    }
121
122    #[test]
123    fn write_rdw_record_emits_header_and_payload() {
124        let mut output = Vec::new();
125        write_record(&mut output, b"ABCD", RecordFormat::RDW).unwrap();
126        assert_eq!(output, vec![0x00, 0x04, 0x00, 0x00, b'A', b'B', b'C', b'D']);
127    }
128
129    #[test]
130    fn write_rdw_record_rejects_oversized_payload() {
131        let mut output = Vec::new();
132        let oversized = vec![0u8; usize::from(u16::MAX) + 1];
133        let err = write_record(&mut output, &oversized, RecordFormat::RDW).unwrap_err();
134        assert_eq!(err.code, ErrorCode::CBKE501_JSON_TYPE_MISMATCH);
135    }
136
137    proptest! {
138        #[test]
139        fn prop_fixed_roundtrip_when_lrecl_equals_payload_len(payload in vec(any::<u8>(), 1..=512)) {
140            let mut encoded = Vec::new();
141            write_record(&mut encoded, &payload, RecordFormat::Fixed).unwrap();
142
143            let mut cursor = Cursor::new(encoded);
144            let decoded = read_record(
145                &mut cursor,
146                RecordFormat::Fixed,
147                Some(u32::try_from(payload.len()).unwrap()),
148            )
149            .unwrap()
150            .unwrap();
151
152            prop_assert_eq!(decoded.as_slice(), payload.as_slice());
153            prop_assert!(read_record(
154                &mut cursor,
155                RecordFormat::Fixed,
156                Some(u32::try_from(payload.len()).unwrap()),
157            ).unwrap().is_none());
158        }
159
160        #[test]
161        fn prop_rdw_roundtrip_preserves_payload(payload in vec(any::<u8>(), 0..=1024)) {
162            let mut encoded = Vec::new();
163            write_record(&mut encoded, &payload, RecordFormat::RDW).unwrap();
164
165            let mut cursor = Cursor::new(encoded);
166            let decoded = read_record(&mut cursor, RecordFormat::RDW, None).unwrap().unwrap();
167            prop_assert_eq!(decoded, payload);
168            prop_assert!(read_record(&mut cursor, RecordFormat::RDW, None).unwrap().is_none());
169        }
170    }
171}