Skip to main content

copybook_codec/
iterator.rs

1// SPDX-License-Identifier: AGPL-3.0-or-later
2//! Record iterator for streaming access to decoded records
3//!
4//! This module provides iterator-based access to records for programmatic processing,
5//! allowing users to process records one at a time without loading entire files into memory.
6//!
7//! # Overview
8//!
9//! The iterator module implements streaming record processing with bounded memory usage.
10//! It provides low-level iterator primitives for reading COBOL data files sequentially,
11//! supporting both fixed-length and RDW (Record Descriptor Word) variable-length formats.
12//!
13//! Key capabilities:
14//!
15//! 1. **Streaming iteration** ([`RecordIterator`]) - Process records one at a time
16//! 2. **Format flexibility** - Handle both fixed-length and RDW variable-length records
17//! 3. **Raw access** ([`RecordIterator::read_raw_record`]) - Access undecoded record bytes
18//! 4. **Convenience functions** ([`iter_records_from_file`], [`iter_records`]) - Simplified creation
19//!
20//! # Performance Characteristics
21//!
22//! The iterator uses buffered I/O and maintains bounded memory usage:
23//! - **Memory**: One record buffer (typically <32 KiB per record)
24//! - **Throughput**: Depends on decode complexity (DISPLAY vs COMP-3)
25//! - **Latency**: Sequential I/O optimized with `BufReader`
26//!
27//! For high-throughput parallel processing, consider using [`crate::decode_file_to_jsonl`]
28//! which provides parallel worker pools and streaming output.
29//!
30//! # Examples
31//!
32//! ## Basic Fixed-Length Record Iteration
33//!
34//! ```rust
35//! use copybook_codec::{iter_records_from_file, DecodeOptions, Codepage, RecordFormat};
36//! use copybook_core::parse_copybook;
37//!
38//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
39//! // Parse copybook schema
40//! let copybook_text = r#"
41//!     01 CUSTOMER-RECORD.
42//!        05 CUSTOMER-ID    PIC 9(5).
43//!        05 CUSTOMER-NAME  PIC X(20).
44//!        05 BALANCE        PIC S9(7)V99 COMP-3.
45//! "#;
46//! let schema = parse_copybook(copybook_text)?;
47//!
48//! // Configure decoding options
49//! let options = DecodeOptions::new()
50//!     .with_codepage(Codepage::CP037)
51//!     .with_format(RecordFormat::Fixed);
52//!
53//! // Create iterator from file
54//! # #[cfg(not(test))]
55//! let iterator = iter_records_from_file("customers.bin", &schema, &options)?;
56//!
57//! // Process records one at a time
58//! # #[cfg(not(test))]
59//! for (index, result) in iterator.enumerate() {
60//!     match result {
61//!         Ok(json_value) => {
62//!             println!("Record {}: {}", index + 1, json_value);
63//!         }
64//!         Err(error) => {
65//!             eprintln!("Error in record {}: {}", index + 1, error);
66//!             break; // Stop on first error
67//!         }
68//!     }
69//! }
70//! # Ok(())
71//! # }
72//! ```
73//!
74//! ## RDW Variable-Length Records
75//!
76//! ```rust
77//! use copybook_codec::{RecordIterator, DecodeOptions, RecordFormat};
78//! use copybook_core::parse_copybook;
79//! use std::fs::File;
80//!
81//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
82//! let copybook_text = r#"
83//!     01 TRANSACTION.
84//!        05 TRAN-ID       PIC 9(10).
85//!        05 TRAN-AMOUNT   PIC S9(9)V99 COMP-3.
86//!        05 TRAN-DESC     PIC X(100).
87//! "#;
88//! let schema = parse_copybook(copybook_text)?;
89//!
90//! let options = DecodeOptions::new()
91//!     .with_format(RecordFormat::RDW);  // RDW variable-length format
92//!
93//! # #[cfg(not(test))]
94//! let file = File::open("transactions.dat")?;
95//! # #[cfg(test)]
96//! # let file = std::io::Cursor::new(vec![]);
97//! let mut iterator = RecordIterator::new(file, &schema, &options)?;
98//!
99//! // Process with error recovery
100//! let mut processed = 0;
101//! let mut errors = 0;
102//!
103//! for (index, result) in iterator.enumerate() {
104//!     match result {
105//!         Ok(json_value) => {
106//!             processed += 1;
107//!             // Process record...
108//!         }
109//!         Err(error) => {
110//!             errors += 1;
111//!             eprintln!("Record {}: {}", index + 1, error);
112//!
113//!             if errors > 10 {
114//!                 eprintln!("Too many errors, stopping");
115//!                 break;
116//!             }
117//!         }
118//!     }
119//! }
120//!
121//! println!("Processed: {}, Errors: {}", processed, errors);
122//! # Ok(())
123//! # }
124//! ```
125//!
126//! ## Raw Record Access (No Decoding)
127//!
128//! ```rust
129//! use copybook_codec::{RecordIterator, DecodeOptions, RecordFormat};
130//! use copybook_core::parse_copybook;
131//! use std::io::Cursor;
132//!
133//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
134//! let copybook_text = "01 RECORD.\n   05 DATA PIC X(10).";
135//! let schema = parse_copybook(copybook_text)?;
136//!
137//! let options = DecodeOptions::new()
138//!     .with_format(RecordFormat::Fixed);
139//!
140//! let data = b"RECORD0001RECORD0002";
141//! let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options)?;
142//!
143//! // Read raw bytes without JSON decoding
144//! while let Some(raw_bytes) = iterator.read_raw_record()? {
145//!     println!("Raw record {}: {} bytes",
146//!              iterator.current_record_index(),
147//!              raw_bytes.len());
148//!
149//!     // Process raw bytes directly...
150//!     // (useful for binary analysis, checksums, etc.)
151//! }
152//! # Ok(())
153//! # }
154//! ```
155//!
156//! ## Collecting Records into a Vec
157//!
158//! ```rust
159//! use copybook_codec::{iter_records, DecodeOptions};
160//! use copybook_core::parse_copybook;
161//! use serde_json::Value;
162//! use std::io::Cursor;
163//!
164//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
165//! let copybook_text = "01 RECORD.\n   05 ID PIC 9(5).";
166//! let schema = parse_copybook(copybook_text)?;
167//! let options = DecodeOptions::default();
168//!
169//! let data = b"0000100002";
170//! let iterator = iter_records(Cursor::new(data), &schema, &options)?;
171//!
172//! // Collect all successful records
173//! let records: Vec<Value> = iterator
174//!     .filter_map(Result::ok)  // Skip errors
175//!     .collect();
176//!
177//! println!("Collected {} records", records.len());
178//! # Ok(())
179//! # }
180//! ```
181//!
182//! ## Using with `DecodeOptions` and Metadata
183//!
184//! ```rust
185//! use copybook_codec::{iter_records_from_file, DecodeOptions, Codepage, JsonNumberMode};
186//! use copybook_core::parse_copybook;
187//!
188//! # fn example() -> Result<(), Box<dyn std::error::Error>> {
189//! let copybook_text = r#"
190//!     01 RECORD.
191//!        05 AMOUNT PIC S9(9)V99 COMP-3.
192//! "#;
193//! let schema = parse_copybook(copybook_text)?;
194//!
195//! // Configure with lossless numbers and metadata
196//! let options = DecodeOptions::new()
197//!     .with_codepage(Codepage::CP037)
198//!     .with_json_number_mode(JsonNumberMode::Lossless)
199//!     .with_emit_meta(true);  // Include field metadata
200//!
201//! # #[cfg(not(test))]
202//! let iterator = iter_records_from_file("data.bin", &schema, &options)?;
203//!
204//! # #[cfg(not(test))]
205//! for result in iterator {
206//!     let json_value = result?;
207//!     // JSON includes metadata: {"AMOUNT": "123.45", "_meta": {...}}
208//!     println!("{}", serde_json::to_string_pretty(&json_value)?);
209//! }
210//! # Ok(())
211//! # }
212//! ```
213
214use crate::options::{DecodeOptions, RecordFormat};
215use copybook_core::{Error, ErrorCode, Result, Schema};
216use copybook_rdw::RdwHeader;
217use serde_json::Value;
218use std::io::{BufReader, Read};
219
220const FIXED_FORMAT_LRECL_MISSING: &str = "Fixed format requires a fixed record length (LRECL). \
221     Set `schema.lrecl_fixed` or use `RecordFormat::Variable`.";
222
223/// Iterator over records in a data file, yielding decoded JSON values
224///
225/// This iterator provides streaming access to records, processing them one at a time
226/// to maintain bounded memory usage even for very large files.
227///
228/// # Examples
229///
230/// ```rust,no_run
231/// use copybook_codec::{RecordIterator, DecodeOptions};
232/// use copybook_core::parse_copybook;
233/// # use std::io::Cursor;
234///
235/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
236/// let copybook_text = "01 RECORD.\n   05 ID PIC 9(5).\n   05 NAME PIC X(20).";
237/// let mut schema = parse_copybook(copybook_text)?;
238/// schema.lrecl_fixed = Some(25);
239/// let options = DecodeOptions::default();
240/// # let record_bytes = b"00001ALICE               ";
241/// # let file = Cursor::new(&record_bytes[..]);
242/// // let file = std::fs::File::open("data.bin")?;
243///
244/// let mut iterator = RecordIterator::new(file, &schema, &options)?;
245///
246/// for (record_index, result) in iterator.enumerate() {
247///     match result {
248///         Ok(json_value) => {
249///             println!("Record {}: {}", record_index + 1, json_value);
250///         }
251///         Err(error) => {
252///             eprintln!("Error in record {}: {}", record_index + 1, error);
253///         }
254///     }
255/// }
256/// # Ok(())
257/// # }
258/// ```
259pub struct RecordIterator<R: Read> {
260    /// The buffered reader
261    reader: BufReader<R>,
262    /// The schema for decoding records
263    schema: Schema,
264    /// Decoding options
265    options: DecodeOptions,
266    /// Current record index (1-based)
267    record_index: u64,
268    /// Whether the iterator has reached EOF
269    eof_reached: bool,
270    /// Buffer for reading record data
271    buffer: Vec<u8>,
272}
273
274impl<R: Read> RecordIterator<R> {
275    /// Create a new record iterator
276    ///
277    /// # Arguments
278    ///
279    /// * `reader` - The input stream to read from
280    /// * `schema` - The parsed copybook schema
281    /// * `options` - Decoding options
282    ///
283    /// # Errors
284    /// Returns an error if the record format is incompatible with the schema.
285    #[inline]
286    #[must_use = "Handle the Result or propagate the error"]
287    pub fn new(reader: R, schema: &Schema, options: &DecodeOptions) -> Result<Self> {
288        Ok(Self {
289            reader: BufReader::new(reader),
290            schema: schema.clone(),
291            options: options.clone(),
292            record_index: 0,
293            eof_reached: false,
294            buffer: Vec::new(),
295        })
296    }
297
298    /// Get the current record index (1-based)
299    ///
300    /// This returns the index of the last record that was successfully read,
301    /// or 0 if no records have been read yet.
302    #[inline]
303    #[must_use]
304    pub fn current_record_index(&self) -> u64 {
305        self.record_index
306    }
307
308    /// Check if the iterator has reached the end of the file
309    #[inline]
310    #[must_use]
311    pub fn is_eof(&self) -> bool {
312        self.eof_reached
313    }
314
315    /// Get a reference to the schema being used
316    #[inline]
317    #[must_use]
318    pub fn schema(&self) -> &Schema {
319        &self.schema
320    }
321
322    /// Get a reference to the decode options being used
323    #[inline]
324    #[must_use]
325    pub fn options(&self) -> &DecodeOptions {
326        &self.options
327    }
328
329    /// Read the next record without decoding it
330    ///
331    /// This method reads the raw bytes of the next record without performing
332    /// JSON decoding. Useful for applications that need access to raw record data
333    /// for binary analysis, checksums, or custom processing.
334    ///
335    /// # Returns
336    ///
337    /// * `Ok(Some(bytes))` - The raw record bytes
338    /// * `Ok(None)` - End of file reached
339    /// * `Err(error)` - An error occurred while reading
340    ///
341    /// # Errors
342    /// Returns an error if underlying I/O operations fail or the record format is invalid.
343    ///
344    /// # Examples
345    ///
346    /// ```rust
347    /// use copybook_codec::{RecordIterator, DecodeOptions, RecordFormat};
348    /// use copybook_core::parse_copybook;
349    /// use std::io::Cursor;
350    ///
351    /// # fn example() -> Result<(), Box<dyn std::error::Error>> {
352    /// let copybook_text = "01 RECORD.\n   05 DATA PIC X(8).";
353    /// let schema = parse_copybook(copybook_text)?;
354    ///
355    /// let options = DecodeOptions::new()
356    ///     .with_format(RecordFormat::Fixed);
357    ///
358    /// let data = b"RECORD01RECORD02";
359    /// let mut iterator = RecordIterator::new(Cursor::new(data), &schema, &options)?;
360    ///
361    /// // Read raw bytes
362    /// if let Some(raw_bytes) = iterator.read_raw_record()? {
363    ///     assert_eq!(raw_bytes, b"RECORD01");
364    ///     assert_eq!(iterator.current_record_index(), 1);
365    /// }
366    ///
367    /// if let Some(raw_bytes) = iterator.read_raw_record()? {
368    ///     assert_eq!(raw_bytes, b"RECORD02");
369    ///     assert_eq!(iterator.current_record_index(), 2);
370    /// }
371    ///
372    /// // End of file
373    /// assert!(iterator.read_raw_record()?.is_none());
374    /// assert!(iterator.is_eof());
375    /// # Ok(())
376    /// # }
377    /// ```
378    #[inline]
379    #[must_use = "Handle the Result or propagate the error"]
380    pub fn read_raw_record(&mut self) -> Result<Option<Vec<u8>>> {
381        if self.eof_reached {
382            return Ok(None);
383        }
384
385        self.buffer.clear();
386
387        let record_data = match self.options.format {
388            RecordFormat::Fixed => {
389                let lrecl = self.schema.lrecl_fixed.ok_or_else(|| {
390                    Error::new(ErrorCode::CBKI001_INVALID_STATE, FIXED_FORMAT_LRECL_MISSING)
391                })? as usize;
392                self.buffer.resize(lrecl, 0);
393
394                match self.reader.read_exact(&mut self.buffer) {
395                    Ok(()) => {
396                        self.record_index += 1;
397                        Some(self.buffer.clone())
398                    }
399                    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
400                        self.eof_reached = true;
401                        return Ok(None);
402                    }
403                    Err(e) => {
404                        return Err(Error::new(
405                            ErrorCode::CBKR101_FIXED_RECORD_ERROR,
406                            format!("Failed to read fixed record: {e}"),
407                        ));
408                    }
409                }
410            }
411            RecordFormat::RDW => {
412                // Read RDW header
413                let mut rdw_header = [0u8; 4];
414                match self.reader.read_exact(&mut rdw_header) {
415                    Ok(()) => {}
416                    Err(e) if e.kind() == std::io::ErrorKind::UnexpectedEof => {
417                        self.eof_reached = true;
418                        return Ok(None);
419                    }
420                    Err(e) => {
421                        return Err(Error::new(
422                            ErrorCode::CBKF221_RDW_UNDERFLOW,
423                            format!("Failed to read RDW header: {e}"),
424                        ));
425                    }
426                }
427
428                // Parse length (payload bytes only)
429                let length = usize::from(RdwHeader::from_bytes(rdw_header).length());
430
431                // Read payload
432                self.buffer.resize(length, 0);
433                match self.reader.read_exact(&mut self.buffer) {
434                    Ok(()) => {
435                        self.record_index += 1;
436                        Some(self.buffer.clone())
437                    }
438                    Err(e) => {
439                        return Err(Error::new(
440                            ErrorCode::CBKF221_RDW_UNDERFLOW,
441                            format!("Failed to read RDW payload: {e}"),
442                        ));
443                    }
444                }
445            }
446        };
447
448        Ok(record_data)
449    }
450
451    /// Decode the next record to JSON
452    ///
453    /// This is the main method used by the Iterator implementation.
454    /// It reads and decodes the next record in one operation.
455    #[inline]
456    fn decode_next_record(&mut self) -> Result<Option<Value>> {
457        match self.read_raw_record()? {
458            Some(record_bytes) => {
459                let json_value = crate::decode_record(&self.schema, &record_bytes, &self.options)?;
460                Ok(Some(json_value))
461            }
462            None => Ok(None),
463        }
464    }
465}
466
467impl<R: Read> Iterator for RecordIterator<R> {
468    type Item = Result<Value>;
469
470    #[inline]
471    fn next(&mut self) -> Option<Self::Item> {
472        if self.eof_reached {
473            return None;
474        }
475
476        match self.decode_next_record() {
477            Ok(Some(value)) => Some(Ok(value)),
478            Ok(None) => {
479                self.eof_reached = true;
480                None
481            }
482            Err(error) => {
483                // On error, we still advance the record index if we were able to read something
484                Some(Err(error))
485            }
486        }
487    }
488}
489
490/// Convenience function to create a record iterator from a file path
491///
492/// This is the most common way to create an iterator for processing COBOL data files.
493/// It handles file opening and iterator creation in a single call.
494///
495/// # Arguments
496///
497/// * `file_path` - Path to the data file
498/// * `schema` - The parsed copybook schema
499/// * `options` - Decoding options
500///
501/// # Errors
502/// Returns an error if the file cannot be opened or the iterator cannot be created.
503///
504/// # Examples
505///
506/// ## Basic Usage with Fixed-Length Records
507///
508/// ```rust,no_run
509/// use copybook_codec::{iter_records_from_file, DecodeOptions, Codepage, RecordFormat};
510/// use copybook_core::parse_copybook;
511///
512/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
513/// let copybook_text = r#"
514///     01 EMPLOYEE-RECORD.
515///        05 EMP-ID        PIC 9(6).
516///        05 EMP-NAME      PIC X(30).
517///        05 EMP-SALARY    PIC S9(7)V99 COMP-3.
518/// "#;
519/// let schema = parse_copybook(copybook_text)?;
520///
521/// let options = DecodeOptions::new()
522///     .with_codepage(Codepage::CP037)
523///     .with_format(RecordFormat::Fixed);
524///
525/// let iterator = iter_records_from_file("employees.dat", &schema, &options)?;
526///
527/// for (index, result) in iterator.enumerate() {
528///     match result {
529///         Ok(employee) => println!("Employee {}: {}", index + 1, employee),
530///         Err(e) => eprintln!("Error at record {}: {}", index + 1, e),
531///     }
532/// }
533/// # Ok(())
534/// # }
535/// ```
536///
537/// ## Processing with Error Limits
538///
539/// ```rust,no_run
540/// use copybook_codec::{iter_records_from_file, DecodeOptions};
541/// use copybook_core::parse_copybook;
542///
543/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
544/// # let schema = parse_copybook("01 R.\n   05 F PIC X(1).")?;
545/// # let options = DecodeOptions::default();
546/// let iterator = iter_records_from_file("data.bin", &schema, &options)?;
547///
548/// let mut success_count = 0;
549/// let mut error_count = 0;
550/// const MAX_ERRORS: usize = 100;
551///
552/// for result in iterator {
553///     match result {
554///         Ok(_) => success_count += 1,
555///         Err(e) => {
556///             error_count += 1;
557///             eprintln!("Error: {}", e);
558///
559///             if error_count >= MAX_ERRORS {
560///                 eprintln!("Too many errors, aborting");
561///                 break;
562///             }
563///         }
564///     }
565/// }
566///
567/// println!("Success: {}, Errors: {}", success_count, error_count);
568/// # Ok(())
569/// # }
570/// ```
571#[inline]
572#[must_use = "Handle the Result or propagate the error"]
573pub fn iter_records_from_file<P: AsRef<std::path::Path>>(
574    file_path: P,
575    schema: &Schema,
576    options: &DecodeOptions,
577) -> Result<RecordIterator<std::fs::File>> {
578    let file = std::fs::File::open(file_path).map_err(|e| {
579        Error::new(
580            ErrorCode::CBKR201_RDW_READ_ERROR,
581            format!("failed to open input file: {e}"),
582        )
583    })?;
584
585    RecordIterator::new(file, schema, options)
586}
587
588/// Convenience function to create a record iterator from any readable source
589///
590/// This function provides maximum flexibility by accepting any type that implements
591/// the `Read` trait, including files, cursors, network streams, or custom readers.
592///
593/// # Arguments
594///
595/// * `reader` - Any type implementing Read (File, Cursor, `TcpStream`, etc.)
596/// * `schema` - The parsed copybook schema
597/// * `options` - Decoding options
598///
599/// # Errors
600/// Returns an error if the iterator cannot be created.
601///
602/// # Examples
603///
604/// ## Using with In-Memory Data (Cursor)
605///
606/// ```rust
607/// use copybook_codec::{iter_records, DecodeOptions, RecordFormat};
608/// use copybook_core::parse_copybook;
609/// use std::io::Cursor;
610///
611/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
612/// let copybook_text = "01 RECORD.\n   05 ID PIC 9(3).\n   05 NAME PIC X(5).";
613/// let schema = parse_copybook(copybook_text)?;
614///
615/// let options = DecodeOptions::new()
616///     .with_format(RecordFormat::Fixed);
617///
618/// // Create iterator from in-memory data
619/// let data = b"001ALICE002BOB  003CAROL";
620/// let iterator = iter_records(Cursor::new(data), &schema, &options)?;
621///
622/// let records: Vec<_> = iterator.collect::<Result<Vec<_>, _>>()?;
623/// assert_eq!(records.len(), 3);
624/// # Ok(())
625/// # }
626/// ```
627///
628/// ## Using with File
629///
630/// ```rust,no_run
631/// use copybook_codec::{iter_records, DecodeOptions};
632/// use copybook_core::parse_copybook;
633/// use std::fs::File;
634///
635/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
636/// let schema = parse_copybook("01 RECORD.\n   05 DATA PIC X(10).")?;
637/// let options = DecodeOptions::default();
638///
639/// let file = File::open("data.bin")?;
640/// let iterator = iter_records(file, &schema, &options)?;
641///
642/// for result in iterator {
643///     let record = result?;
644///     println!("{}", record);
645/// }
646/// # Ok(())
647/// # }
648/// ```
649///
650/// ## Using with Compressed Data
651///
652/// ```text
653/// use copybook_codec::{iter_records, DecodeOptions};
654/// use copybook_core::parse_copybook;
655/// use std::fs::File;
656/// use flate2::read::GzDecoder;
657///
658/// # fn example() -> Result<(), Box<dyn std::error::Error>> {
659/// let schema = parse_copybook("01 RECORD.\n   05 DATA PIC X(10).")?;
660/// let options = DecodeOptions::default();
661///
662/// // Read from gzipped file
663/// let file = File::open("data.bin.gz")?;
664/// let decoder = GzDecoder::new(file);
665/// let iterator = iter_records(decoder, &schema, &options)?;
666///
667/// for result in iterator {
668///     let record = result?;
669///     // Process decompressed record...
670/// }
671/// # Ok(())
672/// # }
673/// ```
674#[inline]
675#[must_use = "Handle the Result or propagate the error"]
676pub fn iter_records<R: Read>(
677    reader: R,
678    schema: &Schema,
679    options: &DecodeOptions,
680) -> Result<RecordIterator<R>> {
681    RecordIterator::new(reader, schema, options)
682}
683
684#[cfg(test)]
685#[allow(clippy::expect_used)]
686#[allow(clippy::unwrap_used)]
687#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
688mod tests {
689    use super::*;
690    use crate::Codepage;
691    use copybook_core::parse_copybook;
692    use std::io::{self, Cursor, Read};
693
694    #[test]
695    fn test_record_iterator_basic() {
696        let copybook_text = r"
697            01 RECORD.
698               05 ID PIC 9(3).
699               05 NAME PIC X(5).
700        ";
701
702        let schema = parse_copybook(copybook_text).unwrap();
703
704        // Create test data: two 8-byte fixed records
705        let test_data = b"001ALICE002BOB  ";
706        let cursor = Cursor::new(test_data);
707
708        let options = DecodeOptions {
709            format: RecordFormat::Fixed,
710            ..DecodeOptions::default()
711        };
712
713        let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
714
715        // Just test that the iterator can be created successfully
716        assert_eq!(iterator.current_record_index(), 0);
717        assert!(!iterator.is_eof());
718    }
719
720    #[test]
721    fn test_record_iterator_rdw() {
722        let copybook_text = r"
723            01 RECORD.
724               05 ID PIC 9(3).
725               05 NAME PIC X(5).
726        ";
727
728        let schema = parse_copybook(copybook_text).unwrap();
729
730        // Create RDW test data:
731        // Record 1: length=8, reserved=0, data="001ALICE"
732        // Record 2: length=6, reserved=0, data="002BOB"
733        let test_data = vec![
734            0x00, 0x08, 0x00, 0x00, // RDW header: length=8, reserved=0
735            b'0', b'0', b'1', b'A', b'L', b'I', b'C', b'E', // Record 1 data
736            0x00, 0x06, 0x00, 0x00, // RDW header: length=6, reserved=0
737            b'0', b'0', b'2', b'B', b'O', b'B', // Record 2 data
738        ];
739
740        let cursor = Cursor::new(test_data);
741
742        let options = DecodeOptions {
743            format: RecordFormat::RDW,
744            ..DecodeOptions::default()
745        };
746
747        let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
748
749        // Just test that the iterator can be created successfully
750        assert_eq!(iterator.current_record_index(), 0);
751        assert!(!iterator.is_eof());
752    }
753
754    #[test]
755    fn test_raw_record_reading() {
756        let copybook_text = r"
757            01 RECORD.
758               05 ID PIC 9(3).
759               05 NAME PIC X(5).
760        ";
761
762        let schema = parse_copybook(copybook_text).unwrap();
763
764        let test_data = b"001ALICE";
765        let cursor = Cursor::new(test_data);
766
767        let options = DecodeOptions {
768            format: RecordFormat::Fixed,
769            ..DecodeOptions::default()
770        };
771
772        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
773
774        // Read raw record
775        let raw_record = iterator.read_raw_record().unwrap().unwrap();
776        assert_eq!(raw_record, b"001ALICE");
777        assert_eq!(iterator.current_record_index(), 1);
778
779        // End of file
780        assert!(iterator.read_raw_record().unwrap().is_none());
781    }
782
783    #[test]
784    fn test_iterator_error_handling() {
785        let copybook_text = r"
786            01 RECORD.
787               05 ID PIC 9(3).
788               05 NAME PIC X(5).
789        ";
790
791        let schema = parse_copybook(copybook_text).unwrap();
792
793        // Create incomplete record (only 4 bytes instead of 8)
794        let test_data = b"001A";
795        let cursor = Cursor::new(test_data);
796
797        let options = DecodeOptions {
798            format: RecordFormat::Fixed,
799            ..DecodeOptions::default()
800        };
801
802        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
803
804        // Should yield EOF (Ok(None)) when encountering truncated fixed-length data
805        assert!(iterator.next().is_none());
806    }
807
808    #[test]
809    fn test_iterator_fixed_format_missing_lrecl_errors_on_next() {
810        // A schema without a fixed record length
811        let copybook_text = "01 SOME-GROUP. 05 SOME-FIELD PIC X(1).";
812        let mut schema = parse_copybook(copybook_text).unwrap();
813        schema.lrecl_fixed = None; // Ensure it's None
814
815        let test_data = b"";
816        let cursor = Cursor::new(test_data);
817
818        let options = DecodeOptions {
819            format: RecordFormat::Fixed,
820            ..DecodeOptions::default()
821        };
822
823        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
824
825        let first = iterator.next().unwrap();
826        assert!(first.is_err());
827        if let Err(e) = first {
828            assert_eq!(e.code, ErrorCode::CBKI001_INVALID_STATE);
829            assert_eq!(e.message, FIXED_FORMAT_LRECL_MISSING);
830        }
831    }
832
833    #[test]
834    fn test_iterator_schema_and_options_accessors() {
835        let copybook_text = r"
836            01 RECORD.
837               05 ID PIC 9(3).
838               05 NAME PIC X(5).
839        ";
840
841        let mut schema = parse_copybook(copybook_text).unwrap();
842        schema.lrecl_fixed = Some(8);
843        let test_data = b"001ALICE";
844        let cursor = Cursor::new(test_data);
845
846        let options = DecodeOptions {
847            format: RecordFormat::Fixed,
848            codepage: Codepage::ASCII,
849            ..DecodeOptions::default()
850        };
851
852        let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
853
854        // Test schema accessor
855        assert_eq!(iterator.schema().fields[0].name, "RECORD");
856
857        // Test options accessor
858        assert_eq!(iterator.options().format, RecordFormat::Fixed);
859    }
860
861    #[test]
862    fn test_iterator_multiple_fixed_records() {
863        let copybook_text = r"
864            01 RECORD.
865               05 ID PIC 9(3).
866               05 NAME PIC X(5).
867        ";
868
869        let mut schema = parse_copybook(copybook_text).unwrap();
870        schema.lrecl_fixed = Some(8);
871
872        // Create test data: three 8-byte fixed records
873        let test_data = b"001ALICE002BOB  003CAROL";
874        let cursor = Cursor::new(test_data);
875
876        let options = DecodeOptions {
877            format: RecordFormat::Fixed,
878            codepage: Codepage::ASCII,
879            ..DecodeOptions::default()
880        };
881
882        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
883
884        // Read all records
885        let mut count = 0;
886        for result in iterator.by_ref() {
887            assert!(result.is_ok(), "Record {count} should decode successfully");
888            count += 1;
889        }
890
891        assert_eq!(count, 3);
892        assert_eq!(iterator.current_record_index(), 3);
893        assert!(iterator.is_eof());
894    }
895
896    #[test]
897    fn test_iterator_rdw_multiple_records() {
898        let copybook_text = r"
899            01 RECORD.
900               05 ID PIC 9(3).
901               05 NAME PIC X(5).
902        ";
903
904        let schema = parse_copybook(copybook_text).unwrap();
905
906        // Create RDW test data with three records
907        let test_data = vec![
908            // Record 1
909            0x00, 0x08, 0x00, 0x00, // RDW header: length=8
910            b'0', b'0', b'1', b'A', b'L', b'I', b'C', b'E', // Record 2
911            0x00, 0x06, 0x00, 0x00, // RDW header: length=6
912            b'0', b'0', b'2', b'B', b'O', b'B', // Record 3
913            0x00, 0x08, 0x00, 0x00, // RDW header: length=8
914            b'0', b'0', b'3', b'C', b'A', b'R', b'O', b'L',
915        ];
916
917        let cursor = Cursor::new(test_data);
918
919        let options = DecodeOptions {
920            format: RecordFormat::RDW,
921            codepage: Codepage::ASCII,
922            ..DecodeOptions::default()
923        };
924
925        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
926
927        // Read all records
928        let mut count = 0;
929        for result in iterator.by_ref() {
930            assert!(result.is_ok(), "Record {count} should decode successfully");
931            count += 1;
932        }
933
934        assert_eq!(count, 3);
935        assert_eq!(iterator.current_record_index(), 3);
936        assert!(iterator.is_eof());
937    }
938
939    #[test]
940    fn test_iter_records_convenience() {
941        let copybook_text = r"
942            01 RECORD.
943               05 ID PIC 9(3).
944               05 NAME PIC X(5).
945        ";
946
947        let schema = parse_copybook(copybook_text).unwrap();
948
949        let test_data = b"001ALICE002BOB  ";
950        let cursor = Cursor::new(test_data);
951
952        let options = DecodeOptions {
953            format: RecordFormat::Fixed,
954            ..DecodeOptions::default()
955        };
956
957        let iterator = iter_records(cursor, &schema, &options).unwrap();
958
959        assert_eq!(iterator.current_record_index(), 0);
960        assert!(!iterator.is_eof());
961    }
962
963    #[test]
964    fn test_iterator_with_empty_data() {
965        let copybook_text = r"
966            01 RECORD.
967               05 ID PIC 9(3).
968               05 NAME PIC X(5).
969        ";
970
971        let mut schema = parse_copybook(copybook_text).unwrap();
972        schema.lrecl_fixed = Some(8);
973
974        let test_data = b"";
975        let cursor = Cursor::new(test_data);
976
977        let options = DecodeOptions {
978            format: RecordFormat::Fixed,
979            ..DecodeOptions::default()
980        };
981
982        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
983
984        // Should immediately return None for empty data
985        assert!(iterator.next().is_none());
986        assert!(iterator.is_eof());
987        assert_eq!(iterator.current_record_index(), 0);
988    }
989
990    #[test]
991    fn test_iterator_raw_record_eof() {
992        let copybook_text = r"
993            01 RECORD.
994               05 ID PIC 9(3).
995               05 NAME PIC X(5).
996        ";
997
998        let schema = parse_copybook(copybook_text).unwrap();
999
1000        let test_data = b"001ALICE";
1001        let cursor = Cursor::new(test_data);
1002
1003        let options = DecodeOptions {
1004            format: RecordFormat::Fixed,
1005            ..DecodeOptions::default()
1006        };
1007
1008        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1009
1010        // Read first record
1011        assert!(iterator.read_raw_record().unwrap().is_some());
1012        assert_eq!(iterator.current_record_index(), 1);
1013
1014        // Read second record (should be None)
1015        assert!(iterator.read_raw_record().unwrap().is_none());
1016        assert!(iterator.is_eof());
1017    }
1018
1019    #[test]
1020    fn test_iterator_collect_results() {
1021        let copybook_text = r"
1022            01 RECORD.
1023               05 ID PIC 9(3).
1024               05 NAME PIC X(5).
1025        ";
1026
1027        let mut schema = parse_copybook(copybook_text).unwrap();
1028        schema.lrecl_fixed = Some(8);
1029
1030        let test_data = b"001ALICE002BOB  003CAROL";
1031        let cursor = Cursor::new(test_data);
1032
1033        let options = DecodeOptions {
1034            format: RecordFormat::Fixed,
1035            codepage: Codepage::ASCII,
1036            ..DecodeOptions::default()
1037        };
1038
1039        let iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1040
1041        // Collect all results
1042        let results: Vec<Result<Value>> = iterator.collect();
1043
1044        assert_eq!(results.len(), 3);
1045        for result in results {
1046            assert!(result.is_ok());
1047        }
1048    }
1049
1050    #[test]
1051    fn test_iterator_with_decode_error() {
1052        let copybook_text = r"
1053            01 RECORD.
1054               05 ID PIC 9(3).
1055               05 NAME PIC X(5).
1056        ";
1057
1058        let mut schema = parse_copybook(copybook_text).unwrap();
1059        schema.lrecl_fixed = Some(8);
1060
1061        // Create data that will decode successfully for first record
1062        let test_data = b"001ALICE";
1063        let cursor = Cursor::new(test_data);
1064
1065        let options = DecodeOptions {
1066            format: RecordFormat::Fixed,
1067            codepage: Codepage::ASCII,
1068            ..DecodeOptions::default()
1069        };
1070
1071        let mut iterator = RecordIterator::new(cursor, &schema, &options).unwrap();
1072
1073        // First record should decode successfully
1074        let first = iterator.next();
1075        assert!(first.is_some());
1076        assert!(first.unwrap().is_ok());
1077
1078        // Second call should return None (EOF)
1079        assert!(iterator.next().is_none());
1080    }
1081
1082    #[derive(Default)]
1083    struct FailingReader {
1084        fail: bool,
1085    }
1086
1087    impl Read for FailingReader {
1088        fn read(&mut self, _buf: &mut [u8]) -> io::Result<usize> {
1089            if self.fail {
1090                Ok(0)
1091            } else {
1092                self.fail = true;
1093                Err(io::Error::other("forced read error"))
1094            }
1095        }
1096    }
1097
1098    #[test]
1099    fn test_iterator_fixed_format_read_error_code() {
1100        let copybook_text = r"
1101            01 RECORD.
1102               05 ID PIC 9(3).
1103               05 NAME PIC X(5).
1104        ";
1105
1106        let schema = parse_copybook(copybook_text).unwrap();
1107
1108        let mut schema = schema;
1109        schema.lrecl_fixed = Some(8);
1110
1111        let mut iterator =
1112            RecordIterator::new(FailingReader::default(), &schema, &DecodeOptions::default())
1113                .unwrap();
1114
1115        let error = iterator.read_raw_record().unwrap_err();
1116        assert_eq!(error.code, ErrorCode::CBKR101_FIXED_RECORD_ERROR);
1117    }
1118}