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::CBKR202_RDW_WRITE_ERROR,
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::CBKR202_RDW_WRITE_ERROR,
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 `CBKF102_RECORD_LENGTH_INVALID` if payload length exceeds
116    /// `u16::MAX`, or an error if writing fails.
117    #[inline]
118    #[must_use = "Handle the Result or propagate the error"]
119    pub fn write_record_from_payload(
120        &mut self,
121        payload: &[u8],
122        preserve_reserved: Option<u16>,
123    ) -> Result<()> {
124        let length = payload.len();
125        let header = RdwHeader::from_payload_len(length, preserve_reserved.unwrap_or(0)).map_err(
126            |error| {
127                error.with_context(ErrorContext {
128                    record_index: Some(self.record_count + 1),
129                    field_path: None,
130                    byte_offset: None,
131                    line_number: None,
132                    details: Some("RDW length field is 16-bit".to_string()),
133                })
134            },
135        )?;
136
137        let record = RDWRecord {
138            header: header.bytes(),
139            payload: payload.to_vec(),
140        };
141        self.write_record(&record)
142    }
143
144    /// Flush writer output.
145    ///
146    /// # Errors
147    /// Returns an error when flush fails.
148    #[inline]
149    #[must_use = "Handle the Result or propagate the error"]
150    pub fn flush(&mut self) -> Result<()> {
151        self.output.flush().map_err(|e| {
152            Error::new(
153                ErrorCode::CBKR202_RDW_WRITE_ERROR,
154                format!("I/O error flushing output: {e}"),
155            )
156        })
157    }
158
159    /// Number of written RDW records.
160    #[inline]
161    #[must_use]
162    pub fn record_count(&self) -> u64 {
163        self.record_count
164    }
165}