Skip to main content

spring_batch_rs/item/csv/
csv_writer.rs

1use std::{cell::RefCell, fs::File, io::Write, marker::PhantomData, path::Path};
2
3use csv::{Writer, WriterBuilder};
4use serde::Serialize;
5
6use crate::{
7    BatchError,
8    core::item::{ItemWriter, ItemWriterResult},
9};
10
11/// A CSV writer that implements the `ItemWriter` trait.
12///
13/// This writer serializes Rust structs to CSV format and writes them to
14/// the underlying destination (file, memory buffer, etc.)
15///
16/// # Type Parameters
17///
18/// - `T`: The type of writer destination, must implement `Write` trait
19///
20/// # Implementation Details
21///
22/// - Uses `RefCell` for interior mutability of the CSV writer
23/// - Integrates with serde for serialization of custom types
24/// - Handles serialization of batch items one by one
25/// - Converts CSV errors to Spring Batch errors
26///
27/// # Ownership Considerations
28///
29/// The writer borrows its destination mutably. When writing to a buffer:
30/// - The buffer will be borrowed for the lifetime of the writer
31/// - To read from the buffer after writing, ensure the writer is dropped first
32/// - One approach is to use a separate scope for the writer operations
33///
34/// # Examples
35///
36/// ```
37/// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
38/// use spring_batch_rs::core::item::ItemWriter;
39/// use serde::Serialize;
40///
41/// #[derive(Serialize)]
42/// struct Record {
43///     id: u32,
44///     name: String,
45/// }
46///
47/// // Create records to write
48/// let records = vec![
49///     Record { id: 1, name: "Alice".to_string() },
50///     Record { id: 2, name: "Bob".to_string() },
51/// ];
52///
53/// // Write records to a CSV string
54/// let mut buffer = Vec::new();
55/// {
56///     // Create a new scope for the writer to ensure it's dropped before we read the buffer
57///     let writer = CsvItemWriterBuilder::new()
58///         .has_headers(true)
59///         .from_writer(&mut buffer);
60///
61///     writer.write(&records).unwrap();
62///     ItemWriter::<Record>::flush(&writer).unwrap();
63/// } // writer is dropped here, releasing the borrow on buffer
64///
65/// // Now we can safely read from the buffer
66/// let csv_content = String::from_utf8(buffer).unwrap();
67/// assert!(csv_content.contains("id,name"));
68/// assert!(csv_content.contains("1,Alice"));
69/// assert!(csv_content.contains("2,Bob"));
70/// ```
71pub struct CsvItemWriter<O, W: Write> {
72    /// The underlying CSV writer
73    ///
74    /// Uses `RefCell` to allow interior mutability while conforming to the
75    /// `ItemWriter` trait's immutable self reference in its methods.
76    writer: RefCell<Writer<W>>,
77    _phantom: PhantomData<O>,
78}
79
80impl<O: Serialize, W: Write> ItemWriter<O> for CsvItemWriter<O, W> {
81    /// Writes a batch of items to CSV.
82    ///
83    /// This method serializes each item in the provided slice to CSV format
84    /// and writes it to the underlying destination.
85    ///
86    /// # Serialization Process
87    ///
88    /// 1. For each item in the batch:
89    ///    - Serialize the item to CSV format using serde
90    ///    - Write the serialized row to the underlying destination
91    /// 2. If any item fails to serialize, return an error immediately
92    ///
93    /// Note: This method doesn't flush the writer. You need to call `flush()`
94    /// explicitly when you're done writing.
95    ///
96    /// # Parameters
97    /// - `items`: A slice of items to be serialized and written
98    ///
99    /// # Returns
100    /// - `Ok(())` if successful
101    /// - `Err(BatchError)` if writing fails
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
107    /// use spring_batch_rs::core::item::ItemWriter;
108    /// use serde::Serialize;
109    ///
110    /// #[derive(Serialize)]
111    /// struct Person {
112    ///     name: String,
113    ///     age: u8,
114    /// }
115    ///
116    /// // Create people to write
117    /// let people = vec![
118    ///     Person { name: "Alice".to_string(), age: 28 },
119    ///     Person { name: "Bob".to_string(), age: 35 },
120    /// ];
121    ///
122    /// // Write to a buffer in a separate scope
123    /// let mut buffer = Vec::new();
124    /// {
125    ///     let writer = CsvItemWriterBuilder::new()
126    ///         .from_writer(&mut buffer);
127    ///
128    ///     // Write the batch of people
129    ///     writer.write(&people).unwrap();
130    ///     ItemWriter::<Person>::flush(&writer).unwrap();
131    /// }
132    /// ```
133    fn write(&self, items: &[O]) -> ItemWriterResult {
134        for item in items.iter() {
135            // Try to serialize each item to CSV format
136            let result = self.writer.borrow_mut().serialize(item);
137
138            // If serialization fails, return the error immediately
139            if result.is_err() {
140                let error = result.err().unwrap();
141                return Err(BatchError::ItemWriter(error.to_string()));
142            }
143        }
144        Ok(())
145    }
146
147    /// Flush the contents of the internal buffer to the underlying writer.
148    ///
149    /// If there was a problem writing to the underlying writer, then an error
150    /// is returned.
151    ///
152    /// Note that this also flushes the underlying writer.
153    ///
154    /// # Important
155    ///
156    /// You must call this method when you're done writing to ensure all data
157    /// is written to the destination. The `write` method buffers data internally
158    /// for efficiency, and `flush` ensures it's all written out.
159    ///
160    /// # When to Call
161    ///
162    /// - After writing all items in a batch
163    /// - Before dropping the writer if you need the data immediately
164    /// - When closing a file to ensure all data is written
165    ///
166    /// # Returns
167    /// - `Ok(())` if successful
168    /// - `Err(BatchError)` if flushing fails
169    ///
170    /// # Examples
171    ///
172    /// ```
173    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
174    /// use spring_batch_rs::core::item::ItemWriter;
175    /// use serde::Serialize;
176    ///
177    /// #[derive(Serialize)]
178    /// struct Record {
179    ///     id: u32,
180    ///     value: String,
181    /// }
182    ///
183    /// // Write to a buffer in a separate scope
184    /// let mut buffer = Vec::new();
185    /// {
186    ///     let writer = CsvItemWriterBuilder::new()
187    ///         .from_writer(&mut buffer);
188    ///
189    ///     // Write some records
190    ///     let records = vec![Record { id: 1, value: "test".to_string() }];
191    ///     writer.write(&records).unwrap();
192    ///
193    ///     // Ensure all data is written - specify type explicitly
194    ///     ItemWriter::<Record>::flush(&writer).unwrap();
195    /// }
196    /// ```
197    fn flush(&self) -> ItemWriterResult {
198        // Flush the underlying CSV writer
199        let result = self.writer.borrow_mut().flush();
200        match result {
201            Ok(()) => Ok(()),
202            Err(error) => Err(BatchError::ItemWriter(error.to_string())),
203        }
204    }
205
206    /// Prepares the writer. CSV has no header ceremony to emit here — headers are
207    /// written lazily by the underlying `csv::Writer` on the first record — so this
208    /// is an explicit no-op provided for symmetry with the JSON and XML writers.
209    ///
210    /// # Returns
211    /// - `Ok(())` always
212    ///
213    /// # Examples
214    ///
215    /// ```
216    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
217    /// use spring_batch_rs::core::item::ItemWriter;
218    /// use serde::Serialize;
219    ///
220    /// #[derive(Serialize)]
221    /// struct Record { id: u32 }
222    ///
223    /// let mut buffer = Vec::new();
224    /// let writer = CsvItemWriterBuilder::<Record>::new().from_writer(&mut buffer);
225    /// assert!(ItemWriter::<Record>::open(&writer).is_ok());
226    /// ```
227    fn open(&self) -> ItemWriterResult {
228        Ok(())
229    }
230
231    /// Finalizes the CSV output by flushing all buffered records.
232    ///
233    /// Without this, buffered rows would only reach the destination when the
234    /// underlying `csv::Writer` is dropped, which discards any I/O error.
235    ///
236    /// # Returns
237    /// - `Ok(())` if all buffered data was written
238    /// - `Err(BatchError::ItemWriter)` if flushing the underlying writer failed
239    ///
240    /// # Examples
241    ///
242    /// ```
243    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
244    /// use spring_batch_rs::core::item::ItemWriter;
245    /// use serde::Serialize;
246    /// use std::cell::RefCell;
247    /// use std::io::Write;
248    /// use std::rc::Rc;
249    ///
250    /// #[derive(Serialize)]
251    /// struct Record { id: u32 }
252    ///
253    /// // A sink whose contents stay readable while the writer is still alive,
254    /// // so the example can show that `close` — not `Drop` — did the flushing.
255    /// struct Sink(Rc<RefCell<Vec<u8>>>);
256    /// impl Write for Sink {
257    ///     fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
258    ///         self.0.borrow_mut().extend_from_slice(buf);
259    ///         Ok(buf.len())
260    ///     }
261    ///     fn flush(&mut self) -> std::io::Result<()> { Ok(()) }
262    /// }
263    ///
264    /// let sink = Rc::new(RefCell::new(Vec::new()));
265    /// let writer = CsvItemWriterBuilder::<Record>::new()
266    ///     .from_writer(Sink(Rc::clone(&sink)));
267    ///
268    /// writer.write(&[Record { id: 7 }]).unwrap();
269    /// assert!(sink.borrow().is_empty(), "csv::Writer still holds the row");
270    ///
271    /// ItemWriter::<Record>::close(&writer).unwrap();
272    ///
273    /// assert!(String::from_utf8(sink.borrow().clone()).unwrap().contains('7'));
274    /// ```
275    fn close(&self) -> ItemWriterResult {
276        self.flush()
277    }
278}
279
280/// A builder for creating CSV item writers.
281///
282/// This builder allows you to customize the CSV writing behavior,
283/// including delimiter and header handling.
284///
285/// # Design Pattern
286///
287/// This struct implements the Builder pattern, which allows for fluent, chainable
288/// configuration of a `CsvItemWriter` before creation. Each method returns `self`
289/// to allow method chaining.
290///
291/// # Default Configuration
292///
293/// - Delimiter: comma (,)
294/// - Headers: disabled (no header row)
295///
296/// # Examples
297///
298/// ```
299/// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
300/// use spring_batch_rs::core::item::ItemWriter;
301/// use serde::Serialize;
302///
303/// #[derive(Serialize)]
304/// struct Record {
305///     id: u32,
306///     name: String,
307/// }
308///
309/// // Create a CSV writer with custom settings
310/// let mut buffer = Vec::new();
311/// let writer = CsvItemWriterBuilder::<Record>::new()
312///     .delimiter(b';')  // Use semicolon as delimiter
313///     .has_headers(true)  // Include headers in output
314///     .from_writer(&mut buffer);
315/// ```
316#[derive(Default)]
317pub struct CsvItemWriterBuilder<O> {
318    /// The delimiter character (default: comma ',')
319    delimiter: u8,
320    /// Whether to include headers in the output (default: false)
321    has_headers: bool,
322    _pd: PhantomData<O>,
323}
324
325impl<O> CsvItemWriterBuilder<O> {
326    /// Creates a new `CsvItemWriterBuilder` with default configuration.
327    ///
328    /// Default settings:
329    /// - Delimiter: comma (,)
330    /// - Headers: disabled
331    ///
332    /// # Examples
333    ///
334    /// ```
335    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
336    /// use serde::Serialize;
337    ///
338    /// #[derive(Serialize)]
339    /// struct Record {
340    ///     field: String,
341    /// }
342    ///
343    /// let builder = CsvItemWriterBuilder::<Record>::new();
344    /// ```
345    pub fn new() -> Self {
346        Self {
347            delimiter: b',',
348            has_headers: false,
349            _pd: PhantomData,
350        }
351    }
352
353    /// Sets the delimiter character for the CSV output.
354    ///
355    /// # Parameters
356    /// - `delimiter`: The character to use as field delimiter
357    ///
358    /// # Common Delimiters
359    ///
360    /// - `b','` - Comma (default in US/UK)
361    /// - `b';'` - Semicolon (common in Europe)
362    /// - `b'\t'` - Tab (for TSV format)
363    /// - `b'|'` - Pipe (less common)
364    ///
365    /// # Examples
366    ///
367    /// ```
368    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
369    /// use serde::Serialize;
370    ///
371    /// #[derive(Serialize)]
372    /// struct Record {
373    ///     field: String,
374    /// }
375    ///
376    /// // Use tab as delimiter
377    /// let builder = CsvItemWriterBuilder::<Record>::new()
378    ///     .delimiter(b'\t');
379    ///
380    /// // Use semicolon as delimiter
381    /// let builder = CsvItemWriterBuilder::<Record>::new()
382    ///     .delimiter(b';');
383    /// ```
384    pub fn delimiter(mut self, delimiter: u8) -> Self {
385        self.delimiter = delimiter;
386        self
387    }
388
389    /// Sets whether to include headers in the CSV output.
390    ///
391    /// When enabled, the writer will include a header row with field names
392    /// derived from the struct field names or serde annotations.
393    ///
394    /// # Parameters
395    /// - `yes`: Whether to include headers
396    ///
397    /// # Header Generation
398    ///
399    /// Headers are generated from:
400    /// - Struct field names by default
401    /// - Custom names specified by `#[serde(rename = "...")]` attributes
402    /// - For nested fields, the serde flattening mechanism is used
403    ///
404    /// # Examples
405    ///
406    /// ```
407    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
408    /// use serde::Serialize;
409    ///
410    /// #[derive(Serialize)]
411    /// struct Record {
412    ///     field: String,
413    /// }
414    ///
415    /// // Include headers (field names as first row)
416    /// let builder = CsvItemWriterBuilder::<Record>::new()
417    ///     .has_headers(true);
418    ///
419    /// // Exclude headers (data only)
420    /// let builder = CsvItemWriterBuilder::<Record>::new()
421    ///     .has_headers(false);
422    /// ```
423    pub fn has_headers(mut self, yes: bool) -> Self {
424        self.has_headers = yes;
425        self
426    }
427
428    /// Creates a CSV item writer that writes to a file.
429    ///
430    /// # Parameters
431    /// - `path`: The path where the output file will be created
432    ///
433    /// # Returns
434    /// A configured `CsvItemWriter` instance
435    ///
436    /// # Panics
437    /// Panics if the file cannot be created
438    ///
439    /// # File Handling
440    ///
441    /// This method will:
442    /// - Create the file if it doesn't exist
443    /// - Truncate the file if it exists
444    /// - Return a writer that writes to the file
445    ///
446    /// # Examples
447    ///
448    /// ```no_run
449    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
450    /// use spring_batch_rs::core::item::ItemWriter;
451    /// use serde::Serialize;
452    ///
453    /// #[derive(Serialize)]
454    /// struct Record {
455    ///     id: u32,
456    ///     value: String,
457    /// }
458    ///
459    /// // Create a writer to a file
460    /// let writer = CsvItemWriterBuilder::<Record>::new()
461    ///     .has_headers(true)
462    ///     .from_path("output.csv");
463    ///
464    /// // Write some data
465    /// let records = vec![
466    ///     Record { id: 1, value: "data1".to_string() },
467    ///     Record { id: 2, value: "data2".to_string() },
468    /// ];
469    ///
470    /// writer.write(&records).unwrap();
471    /// ItemWriter::<Record>::flush(&writer).unwrap();
472    /// ```
473    pub fn from_path<W: AsRef<Path>>(self, path: W) -> CsvItemWriter<O, File> {
474        // Configure and create the CSV writer
475        let writer = WriterBuilder::new()
476            .flexible(false) // Use strict formatting to detect serialization issues
477            .has_headers(self.has_headers)
478            .delimiter(self.delimiter)
479            .from_path(path);
480
481        // Unwrap here is appropriate since file opening is an initialization step
482        // If it fails, we want to fail fast
483        CsvItemWriter {
484            writer: RefCell::new(writer.unwrap()),
485            _phantom: PhantomData,
486        }
487    }
488
489    /// Creates a CSV item writer that writes to any destination implementing the `Write` trait.
490    ///
491    /// This allows writing to in-memory buffers, network connections, or other custom destinations.
492    ///
493    /// # Parameters
494    /// - `wtr`: The writer instance to use for output
495    ///
496    /// # Returns
497    /// A configured `CsvItemWriter` instance
498    ///
499    /// # Common Writer Types
500    ///
501    /// - `&mut Vec<u8>` - In-memory buffer (most common for tests)
502    /// - `File` - File writer for permanent storage
503    /// - `Cursor<Vec<u8>>` - In-memory cursor for testing
504    /// - `TcpStream` - Network connection for remote writing
505    ///
506    /// # Examples
507    ///
508    /// ```
509    /// use spring_batch_rs::item::csv::csv_writer::CsvItemWriterBuilder;
510    /// use spring_batch_rs::core::item::ItemWriter;
511    /// use serde::Serialize;
512    ///
513    /// #[derive(Serialize)]
514    /// struct Row<'a> {
515    ///     city: &'a str,
516    ///     country: &'a str,
517    ///     #[serde(rename = "popcount")]
518    ///     population: u64,
519    /// }
520    ///
521    /// // Prepare some data
522    /// let rows = vec![
523    ///     Row {
524    ///         city: "Boston",
525    ///         country: "United States",
526    ///         population: 4628910,
527    ///     },
528    ///     Row {
529    ///         city: "Concord",
530    ///         country: "United States",
531    ///         population: 42695,
532    ///     }
533    /// ];
534    ///
535    /// // Write to a vector buffer in a separate scope
536    /// let mut buffer = Vec::new();
537    /// {
538    ///     let writer = CsvItemWriterBuilder::<Row>::new()
539    ///         .has_headers(true)
540    ///         .from_writer(&mut buffer);
541    ///
542    ///     // Write the data
543    ///     writer.write(&rows).unwrap();
544    ///     ItemWriter::<Row>::flush(&writer).unwrap();
545    /// } // writer is dropped here, releasing the borrow
546    ///
547    /// // Check the output (with headers)
548    /// let output = String::from_utf8(buffer).unwrap();
549    /// assert!(output.contains("city,country,popcount"));
550    /// assert!(output.contains("Boston,United States,4628910"));
551    /// ```
552    pub fn from_writer<W: Write>(self, wtr: W) -> CsvItemWriter<O, W> {
553        // Configure and create the CSV writer
554        let wtr = WriterBuilder::new()
555            .flexible(false) // Use strict formatting to detect serialization issues
556            .has_headers(self.has_headers)
557            .delimiter(self.delimiter)
558            .from_writer(wtr);
559
560        CsvItemWriter {
561            writer: RefCell::new(wtr),
562            _phantom: PhantomData,
563        }
564    }
565}
566
567#[cfg(test)]
568mod tests {
569    use super::*;
570    use crate::core::item::ItemWriter;
571    use serde::Serialize;
572
573    #[derive(Serialize, Clone)]
574    struct Row {
575        name: String,
576        value: u32,
577    }
578
579    fn sample_rows() -> Vec<Row> {
580        vec![
581            Row {
582                name: "alpha".into(),
583                value: 1,
584            },
585            Row {
586                name: "beta".into(),
587                value: 2,
588            },
589        ]
590    }
591
592    #[test]
593    fn should_write_records_with_headers() {
594        let mut buf = Vec::new();
595        {
596            let writer = CsvItemWriterBuilder::<Row>::new()
597                .has_headers(true)
598                .from_writer(&mut buf);
599            writer.write(&sample_rows()).unwrap();
600            ItemWriter::<Row>::flush(&writer).unwrap();
601        }
602        let out = String::from_utf8(buf).unwrap();
603        assert!(out.contains("name,value"), "header row missing: {out}");
604        assert!(out.contains("alpha,1"), "first data row missing: {out}");
605        assert!(out.contains("beta,2"), "second data row missing: {out}");
606    }
607
608    #[test]
609    fn should_write_records_without_headers() {
610        let mut buf = Vec::new();
611        {
612            let writer = CsvItemWriterBuilder::<Row>::new()
613                .has_headers(false)
614                .from_writer(&mut buf);
615            writer.write(&sample_rows()).unwrap();
616            ItemWriter::<Row>::flush(&writer).unwrap();
617        }
618        let out = String::from_utf8(buf).unwrap();
619        assert!(!out.contains("name"), "header row should be absent: {out}");
620        assert!(
621            out.contains("alpha,1"),
622            "data row missing from headerless output: {out}"
623        );
624    }
625
626    #[test]
627    fn should_write_with_custom_delimiter() {
628        let mut buf = Vec::new();
629        {
630            let writer = CsvItemWriterBuilder::<Row>::new()
631                .has_headers(true)
632                .delimiter(b';')
633                .from_writer(&mut buf);
634            writer.write(&sample_rows()).unwrap();
635            ItemWriter::<Row>::flush(&writer).unwrap();
636        }
637        let out = String::from_utf8(buf).unwrap();
638        assert!(
639            out.contains("name;value"),
640            "semicolon header missing: {out}"
641        );
642        assert!(out.contains("alpha;1"), "semicolon data missing: {out}");
643    }
644
645    #[test]
646    fn should_write_empty_chunk_without_error() {
647        let mut buf = Vec::new();
648        {
649            let writer = CsvItemWriterBuilder::<Row>::new()
650                .has_headers(true)
651                .from_writer(&mut buf);
652            writer.write(&[]).unwrap();
653            ItemWriter::<Row>::flush(&writer).unwrap();
654        }
655        let out = String::from_utf8(buf).unwrap();
656        assert!(
657            out.is_empty(),
658            "writing an empty chunk should produce no output, got: {out:?}"
659        );
660    }
661
662    #[test]
663    fn should_write_single_record() {
664        let mut buf = Vec::new();
665        {
666            let writer = CsvItemWriterBuilder::<Row>::new()
667                .has_headers(false)
668                .from_writer(&mut buf);
669            writer
670                .write(&[Row {
671                    name: "only".into(),
672                    value: 99,
673                }])
674                .unwrap();
675            ItemWriter::<Row>::flush(&writer).unwrap();
676        }
677        let out = String::from_utf8(buf).unwrap();
678        assert!(out.contains("only,99"), "single record missing: {out}");
679    }
680
681    #[test]
682    fn should_return_error_when_serialization_fails() {
683        use serde::ser;
684
685        #[derive(Clone)]
686        struct FailSerialize;
687        impl Serialize for FailSerialize {
688            fn serialize<S: serde::Serializer>(&self, _s: S) -> Result<S::Ok, S::Error> {
689                Err(ser::Error::custom("intentional failure"))
690            }
691        }
692
693        let mut buf = Vec::new();
694        let writer = CsvItemWriterBuilder::<FailSerialize>::new().from_writer(&mut buf);
695        let result = writer.write(&[FailSerialize]);
696        assert!(result.is_err(), "should fail when serialization fails");
697        match result {
698            Err(BatchError::ItemWriter(_)) => {}
699            other => panic!("expected ItemWriter error, got {other:?}"),
700        }
701    }
702
703    #[test]
704    fn should_return_error_when_flush_fails_on_io() {
705        use std::io;
706
707        struct FailFlushWriter;
708        impl Write for FailFlushWriter {
709            fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
710                Ok(buf.len())
711            }
712            fn flush(&mut self) -> io::Result<()> {
713                Err(io::Error::new(io::ErrorKind::Other, "flush failed"))
714            }
715        }
716
717        let csv_writer = CsvItemWriter::<Row, FailFlushWriter> {
718            writer: RefCell::new(WriterBuilder::new().from_writer(FailFlushWriter)),
719            _phantom: PhantomData,
720        };
721        let result = ItemWriter::<Row>::flush(&csv_writer);
722        assert!(
723            result.is_err(),
724            "flush should fail when underlying writer fails"
725        );
726        match result {
727            Err(BatchError::ItemWriter(_)) => {}
728            other => panic!("expected ItemWriter error, got {other:?}"),
729        }
730    }
731
732    #[test]
733    fn should_write_to_file() {
734        use std::fs;
735        use tempfile::NamedTempFile;
736
737        let tmp = NamedTempFile::new().unwrap();
738        let path = tmp.path().to_path_buf();
739
740        let writer = CsvItemWriterBuilder::<Row>::new()
741            .has_headers(true)
742            .from_path(&path);
743        writer.write(&sample_rows()).unwrap();
744        ItemWriter::<Row>::flush(&writer).unwrap();
745        drop(writer);
746
747        let content = fs::read_to_string(&path).unwrap();
748        assert!(content.contains("name,value"), "file header missing");
749        assert!(content.contains("alpha,1"), "file data missing");
750    }
751
752    /// A `Write` sink whose contents the test can inspect while the writer is still alive.
753    #[derive(Clone)]
754    struct SharedBuffer(std::rc::Rc<std::cell::RefCell<Vec<u8>>>);
755
756    impl std::io::Write for SharedBuffer {
757        fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
758            self.0.borrow_mut().extend_from_slice(buf);
759            Ok(buf.len())
760        }
761
762        fn flush(&mut self) -> std::io::Result<()> {
763            Ok(())
764        }
765    }
766
767    #[test]
768    fn should_flush_pending_rows_on_close() {
769        #[derive(Serialize)]
770        struct Row {
771            name: String,
772            value: u32,
773        }
774
775        let shared = std::rc::Rc::new(std::cell::RefCell::new(Vec::new()));
776        let writer = CsvItemWriterBuilder::<Row>::new()
777            .has_headers(true)
778            .from_writer(SharedBuffer(std::rc::Rc::clone(&shared)));
779
780        writer
781            .write(&[Row {
782                name: "alpha".to_string(),
783                value: 1,
784            }])
785            .unwrap();
786
787        // csv::Writer buffers internally, so nothing has reached the sink yet.
788        assert!(
789            shared.borrow().is_empty(),
790            "expected the row to still be buffered before close, sink already had: {:?}",
791            String::from_utf8_lossy(&shared.borrow())
792        );
793
794        ItemWriter::<Row>::close(&writer).unwrap();
795
796        // Read the sink while `writer` is still in scope — Drop has NOT run yet,
797        // so anything visible here was flushed by close() itself.
798        let output = String::from_utf8(shared.borrow().clone()).unwrap();
799        assert!(output.contains("alpha,1"), "close did not flush: {output}");
800    }
801
802    #[test]
803    fn should_return_ok_from_open() {
804        #[derive(Serialize)]
805        struct Row {
806            name: String,
807        }
808
809        let mut buffer = Vec::new();
810        let writer = CsvItemWriterBuilder::<Row>::new().from_writer(&mut buffer);
811
812        assert!(ItemWriter::<Row>::open(&writer).is_ok());
813    }
814}