Skip to main content

copybook_rdw/
writer.rs

1use crate::{RDW_MAX_PAYLOAD_LEN, RDWRecord, RdwHeader};
2use copybook_error::{Error, ErrorCode, ErrorContext, Result};
3use std::io::Write;
4use tracing::debug;
5
6/// RDW record writer for variable-length records.
7#[derive(Debug)]
8pub struct RDWRecordWriter<W: Write> {
9    output: W,
10    record_count: u64,
11}
12
13impl<W: Write> RDWRecordWriter<W> {
14    /// Create a new RDW record writer.
15    #[inline]
16    #[must_use]
17    pub fn new(output: W) -> Self {
18        Self {
19            output,
20            record_count: 0,
21        }
22    }
23
24    /// Write an RDW record.
25    ///
26    /// # Errors
27    /// Returns an error if writing header or payload fails.
28    #[inline]
29    #[must_use = "Handle the Result or propagate the error"]
30    pub fn write_record(&mut self, record: &RDWRecord) -> Result<()> {
31        self.validate_record(record)?;
32
33        self.output.write_all(&record.header).map_err(|e| {
34            Error::new(
35                ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
36                format!("I/O error writing RDW header: {e}"),
37            )
38            .with_context(ErrorContext {
39                record_index: Some(self.record_count + 1),
40                field_path: None,
41                byte_offset: None,
42                line_number: None,
43                details: None,
44            })
45        })?;
46
47        self.output.write_all(&record.payload).map_err(|e| {
48            Error::new(
49                ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
50                format!("I/O error writing RDW payload: {e}"),
51            )
52            .with_context(ErrorContext {
53                record_index: Some(self.record_count + 1),
54                field_path: None,
55                byte_offset: Some(4),
56                line_number: None,
57                details: None,
58            })
59        })?;
60
61        self.record_count += 1;
62        debug!(
63            "Wrote RDW record {} with {} byte payload",
64            self.record_count,
65            record.payload.len()
66        );
67        Ok(())
68    }
69
70    #[inline]
71    fn validate_record(&self, record: &RDWRecord) -> Result<()> {
72        let header_len = usize::from(record.length());
73        let payload_len = record.payload.len();
74
75        if payload_len > RDW_MAX_PAYLOAD_LEN {
76            return Err(Error::new(
77                ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
78                format!(
79                    "RDW payload too large: {payload_len} bytes exceeds maximum of {RDW_MAX_PAYLOAD_LEN}"
80                ),
81            )
82            .with_context(ErrorContext {
83                record_index: Some(self.record_count + 1),
84                field_path: None,
85                byte_offset: None,
86                line_number: None,
87                details: Some("RDW length field is 16-bit".to_string()),
88            }));
89        }
90
91        if header_len != payload_len {
92            return Err(Error::new(
93                ErrorCode::CBKF102_RECORD_LENGTH_INVALID,
94                format!(
95                    "RDW header length mismatch: header declares {header_len} bytes, payload has {payload_len} bytes"
96                ),
97            )
98            .with_context(ErrorContext {
99                record_index: Some(self.record_count + 1),
100                field_path: None,
101                byte_offset: Some(0),
102                line_number: None,
103                details: Some(
104                    "RDW header length must match payload length before writing".to_string(),
105                ),
106            }));
107        }
108
109        Ok(())
110    }
111
112    /// Write an RDW record directly from payload.
113    ///
114    /// # Errors
115    /// Returns an error if payload length exceeds `u16::MAX` or I/O fails.
116    #[inline]
117    #[must_use = "Handle the Result or propagate the error"]
118    pub fn write_record_from_payload(
119        &mut self,
120        payload: &[u8],
121        preserve_reserved: Option<u16>,
122    ) -> Result<()> {
123        let length = payload.len();
124        let header =
125            RdwHeader::from_payload_len(length, preserve_reserved.unwrap_or(0)).map_err(|_| {
126                Error::new(
127                    ErrorCode::CBKE501_JSON_TYPE_MISMATCH,
128                    format!(
129                        "RDW payload too large: {length} bytes exceeds maximum of {}",
130                        u16::MAX
131                    ),
132                )
133                .with_context(ErrorContext {
134                    record_index: Some(self.record_count + 1),
135                    field_path: None,
136                    byte_offset: None,
137                    line_number: None,
138                    details: Some("RDW length field is 16-bit".to_string()),
139                })
140            })?;
141
142        let record = RDWRecord {
143            header: header.bytes(),
144            payload: payload.to_vec(),
145        };
146        self.write_record(&record)
147    }
148
149    /// Flush writer output.
150    ///
151    /// # Errors
152    /// Returns an error when flush fails.
153    #[inline]
154    #[must_use = "Handle the Result or propagate the error"]
155    pub fn flush(&mut self) -> Result<()> {
156        self.output.flush().map_err(|e| {
157            Error::new(
158                ErrorCode::CBKF104_RDW_SUSPECT_ASCII,
159                format!("I/O error flushing output: {e}"),
160            )
161        })
162    }
163
164    /// Number of written RDW records.
165    #[inline]
166    #[must_use]
167    pub fn record_count(&self) -> u64 {
168        self.record_count
169    }
170}