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}