Skip to main content

spring_batch_rs/item/xml/
xml_writer.rs

1use crate::core::item::{ItemWriter, ItemWriterResult};
2use crate::error::BatchError;
3use quick_xml::{
4    events::{BytesEnd, BytesStart, Event},
5    Writer,
6};
7use serde::Serialize;
8use std::cell::RefCell;
9use std::fs::File;
10use std::io::{BufWriter, Write};
11use std::marker::PhantomData;
12use std::path::Path;
13
14/// A writer that writes items to an XML file.
15///
16/// # Examples
17///
18/// ```
19/// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
20/// use spring_batch_rs::core::item::ItemWriter;
21/// use serde::Serialize;
22/// use std::io::Cursor;
23///
24/// #[derive(Serialize)]
25/// struct Person {
26///     #[serde(rename = "@id")]
27///     id: i32,
28///     name: String,
29///     age: i32,
30/// }
31///
32/// // Create a writer that writes to a memory buffer
33/// let buffer = Cursor::new(Vec::new());
34/// let writer = XmlItemWriterBuilder::<Person>::new()
35///     .root_tag("people")
36///     .item_tag("person")
37///     .from_writer(buffer);
38///
39/// // Create some data to write
40/// let persons = vec![
41///     Person { id: 1, name: "Alice".to_string(), age: 30 },
42///     Person { id: 2, name: "Bob".to_string(), age: 25 },
43/// ];
44///
45/// // Write the data
46/// writer.open().unwrap();
47/// writer.write(&persons).unwrap();
48/// writer.close().unwrap();
49/// ```
50///
51/// Using a file as output:
52///
53/// ```no_run
54/// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
55/// use spring_batch_rs::core::item::ItemWriter;
56/// use serde::Serialize;
57/// use tempfile::NamedTempFile;
58///
59/// #[derive(Serialize)]
60/// struct Person {
61///     #[serde(rename = "@id")]
62///     id: i32,
63///     name: String,
64///     age: i32,
65/// }
66///
67/// // Create a temporary file
68/// let temp_file = NamedTempFile::new().unwrap();
69/// let writer = XmlItemWriterBuilder::<Person>::new()
70///     .root_tag("people")
71///     .item_tag("person")
72///     .from_path(temp_file.path())
73///     .unwrap();
74///
75/// // Create some data to write
76/// let persons = vec![
77///     Person { id: 1, name: "Alice".to_string(), age: 30 },
78///     Person { id: 2, name: "Bob".to_string(), age: 25 },
79/// ];
80///
81/// // Write the data
82/// writer.open().unwrap();
83/// writer.write(&persons).unwrap();
84/// writer.close().unwrap();
85///
86/// // The XML file now contains:
87/// // <people>
88/// //   <person id="1">
89/// //     <name>Alice</name>
90/// //     <age>30</age>
91/// //   </person>
92/// //   <person id="2">
93/// //     <name>Bob</name>
94/// //     <age>25</age>
95/// //   </person>
96/// // </people>
97/// ```
98pub struct XmlItemWriter<O, W: Write = File> {
99    writer: RefCell<Writer<BufWriter<W>>>,
100    item_tag: String,
101    root_tag: String,
102    _phantom: PhantomData<O>,
103}
104
105impl<O, W: Write> ItemWriter<O> for XmlItemWriter<O, W>
106where
107    O: Serialize,
108{
109    fn write(&self, items: &[O]) -> ItemWriterResult {
110        for item in items {
111            self.writer
112                .borrow_mut()
113                .write_serializable(&self.item_tag, item)
114                .map_err(|e| BatchError::ItemWriter(format!("Failed to write XML item: {}", e)))?;
115        }
116        Ok(())
117    }
118
119    fn flush(&self) -> ItemWriterResult {
120        let result = self.writer.borrow_mut().get_mut().flush();
121        match result {
122            Ok(()) => Ok(()),
123            Err(e) => Err(BatchError::ItemWriter(format!(
124                "Failed to flush XML file: {}",
125                e
126            ))),
127        }
128    }
129
130    fn open(&self) -> ItemWriterResult {
131        let root = BytesStart::new(&self.root_tag);
132        self.writer
133            .borrow_mut()
134            .write_event(Event::Start(root))
135            .map_err(|e| BatchError::ItemWriter(format!("Failed to write XML root: {}", e)))?;
136        Ok(())
137    }
138
139    fn close(&self) -> ItemWriterResult {
140        self.writer
141            .borrow_mut()
142            .write_event(Event::End(BytesEnd::new(&self.root_tag)))
143            .map_err(|e| BatchError::ItemWriter(format!("Failed to write XML end: {}", e)))?;
144        self.flush()
145    }
146}
147
148/// Builder for creating XML item writers.
149///
150/// This builder allows you to configure XML writers with:
151/// - A root tag for the XML document
152/// - An item tag for each written element
153/// - Various output destinations (file, in-memory buffer, etc.)
154///
155/// # Examples
156///
157/// ```
158/// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
159/// use spring_batch_rs::core::item::ItemWriter;
160/// use serde::Serialize;
161/// use std::io::Cursor;
162///
163/// #[derive(Serialize)]
164/// struct Address {
165///     street: String,
166///     city: String,
167///     country: String,
168/// }
169///
170/// #[derive(Serialize)]
171/// struct Person {
172///     #[serde(rename = "@id")]
173///     id: i32,
174///     name: String,
175///     age: i32,
176///     address: Address,
177/// }
178///
179/// // Create a buffer for our output
180/// let buffer = Cursor::new(Vec::new());
181///
182/// // Create a writer using the builder pattern
183/// let writer = XmlItemWriterBuilder::<Person>::new()
184///     .root_tag("directory")
185///     .item_tag("person")
186///     .from_writer(buffer);
187///
188/// // Create a person with nested address
189/// let person = Person {
190///     id: 1,
191///     name: "Alice".to_string(),
192///     age: 30,
193///     address: Address {
194///         street: "123 Main St".to_string(),
195///         city: "Springfield".to_string(),
196///         country: "USA".to_string(),
197///     },
198/// };
199///
200/// // Write the person to XML
201/// writer.open().unwrap();
202/// writer.write(&[person]).unwrap();
203/// writer.close().unwrap();
204/// ```
205#[derive(Default)]
206pub struct XmlItemWriterBuilder<O> {
207    root_tag: String,
208    item_tag: Option<String>,
209    _pd: PhantomData<O>,
210}
211
212impl<O> XmlItemWriterBuilder<O> {
213    /// Creates a new `XmlItemWriterBuilder` with default values.
214    ///
215    /// The default root tag is "root" and the default item tag is derived from
216    /// the type name of the serialized items.
217    ///
218    /// # Examples
219    ///
220    /// ```
221    /// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
222    /// use serde::Serialize;
223    ///
224    /// #[derive(Serialize)]
225    /// struct Record {
226    ///     field: String,
227    /// }
228    ///
229    /// let builder = XmlItemWriterBuilder::<Record>::new();
230    /// ```
231    pub fn new() -> Self {
232        Self {
233            root_tag: "root".to_string(),
234            item_tag: None,
235            _pd: PhantomData,
236        }
237    }
238
239    /// Sets the root tag for the XML document.
240    ///
241    /// The root tag wraps all items in the XML document.
242    ///
243    /// # Examples
244    ///
245    /// ```
246    /// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
247    /// use serde::Serialize;
248    ///
249    /// #[derive(Serialize)]
250    /// struct Person {
251    ///     name: String,
252    /// }
253    ///
254    /// let builder = XmlItemWriterBuilder::<Person>::new()
255    ///     .root_tag("people");
256    /// ```
257    pub fn root_tag(mut self, root_tag: &str) -> Self {
258        self.root_tag = root_tag.to_string();
259        self
260    }
261
262    /// Sets the item tag for each XML element.
263    ///
264    /// Each item in the collection will be wrapped with this tag.
265    /// If not specified, it will default to the lowercase name of the item type.
266    ///
267    /// # Examples
268    ///
269    /// ```
270    /// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
271    /// use serde::Serialize;
272    ///
273    /// #[derive(Serialize)]
274    /// struct Person {
275    ///     name: String,
276    /// }
277    ///
278    /// let builder = XmlItemWriterBuilder::<Person>::new()
279    ///     .root_tag("people")
280    ///     .item_tag("person");
281    /// ```
282    pub fn item_tag(mut self, item_tag: &str) -> Self {
283        self.item_tag = Some(item_tag.to_string());
284        self
285    }
286
287    /// Creates an `XmlItemWriter` from a file path.
288    ///
289    /// # Examples
290    ///
291    /// ```no_run
292    /// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
293    /// use serde::Serialize;
294    /// use tempfile::NamedTempFile;
295    ///
296    /// #[derive(Serialize)]
297    /// struct Person {
298    ///     name: String,
299    ///     age: i32,
300    /// }
301    ///
302    /// // Create a temporary file for testing
303    /// let temp_file = NamedTempFile::new().unwrap();
304    /// let writer = XmlItemWriterBuilder::<Person>::new()
305    ///     .root_tag("people")
306    ///     .item_tag("person")
307    ///     .from_path(temp_file.path())
308    ///     .unwrap();
309    /// ```
310    pub fn from_path<P: AsRef<Path>>(self, path: P) -> Result<XmlItemWriter<O>, BatchError> {
311        let file = File::create(path)
312            .map_err(|e| BatchError::ItemWriter(format!("Failed to create XML file: {}", e)))?;
313        let writer = Writer::new(BufWriter::new(file));
314        let item_tag = self.item_tag.unwrap_or_else(|| {
315            std::any::type_name::<O>()
316                .split("::")
317                .last()
318                .unwrap_or("item")
319                .to_lowercase()
320        });
321
322        Ok(XmlItemWriter {
323            writer: RefCell::new(writer),
324            item_tag,
325            root_tag: self.root_tag,
326            _phantom: PhantomData,
327        })
328    }
329
330    /// Creates an `XmlItemWriter` from a writer.
331    ///
332    /// This is useful for writing to in-memory buffers, network streams,
333    /// or other custom writers.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use spring_batch_rs::item::xml::xml_writer::XmlItemWriterBuilder;
339    /// use spring_batch_rs::core::item::ItemWriter;
340    /// use serde::Serialize;
341    /// use std::io::Cursor;
342    ///
343    /// #[derive(Serialize)]
344    /// struct Person {
345    ///     name: String,
346    ///     age: i32,
347    /// }
348    ///
349    /// // Create a writer that writes to an in-memory buffer
350    /// let buffer = Cursor::new(Vec::new());
351    /// let writer = XmlItemWriterBuilder::<Person>::new()
352    ///     .root_tag("people")
353    ///     .item_tag("person")
354    ///     .from_writer(buffer);
355    ///
356    /// // Now we can use the writer to write XML data
357    /// writer.open().unwrap();
358    /// writer.write(&[Person { name: "Alice".to_string(), age: 30 }]).unwrap();
359    /// writer.close().unwrap();
360    /// ```
361    pub fn from_writer<W: Write>(self, wtr: W) -> XmlItemWriter<O, W> {
362        let writer = Writer::new(BufWriter::new(wtr));
363        let item_tag = self.item_tag.unwrap_or_else(|| {
364            std::any::type_name::<O>()
365                .split("::")
366                .last()
367                .unwrap_or("item")
368                .to_lowercase()
369        });
370
371        XmlItemWriter {
372            writer: RefCell::new(writer),
373            item_tag,
374            root_tag: self.root_tag,
375            _phantom: PhantomData,
376        }
377    }
378}
379
380#[cfg(test)]
381mod tests {
382    use super::*;
383    use serde::{Deserialize, Serialize};
384    use std::io::Cursor;
385    use tempfile::NamedTempFile;
386
387    #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
388    struct Contact {
389        #[serde(rename = "@type")]
390        contact_type: String,
391        name: String,
392        email: String,
393        phone: String,
394    }
395
396    #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
397    struct Location {
398        #[serde(rename = "@country")]
399        country: String,
400        city: String,
401        #[serde(rename = "@timezone")]
402        timezone: String,
403        coordinates: Coordinates,
404    }
405
406    #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
407    struct Coordinates {
408        #[serde(rename = "@format")]
409        format: String,
410        latitude: f64,
411        longitude: f64,
412    }
413
414    #[derive(Debug, Deserialize, Serialize, PartialEq, Clone)]
415    struct Company {
416        #[serde(rename = "@id")]
417        id: i32,
418        #[serde(rename = "@type")]
419        company_type: String,
420        name: String,
421        founded_year: i32,
422        contact: Vec<Contact>,
423        location: Location,
424        #[serde(rename = "@active")]
425        active: bool,
426    }
427
428    #[derive(Debug, Serialize, Deserialize, PartialEq)]
429    struct SimpleItem {
430        id: i32,
431        name: String,
432        value: f64,
433    }
434
435    #[derive(Debug, Serialize, Deserialize, PartialEq)]
436    struct Product {
437        id: i32,
438        name: String,
439        price: f64,
440        tags: Vec<String>,
441    }
442
443    #[test]
444    fn test_xml_writer_builder() {
445        let temp_file = NamedTempFile::new().unwrap();
446        let writer = XmlItemWriterBuilder::<Company>::new()
447            .root_tag("companies")
448            .item_tag("company")
449            .from_path(temp_file.path())
450            .unwrap();
451
452        let items = vec![
453            Company {
454                id: 1,
455                company_type: "tech".to_string(),
456                name: "TechCorp".to_string(),
457                founded_year: 2010,
458                active: true,
459                contact: vec![
460                    Contact {
461                        contact_type: "primary".to_string(),
462                        name: "John Doe".to_string(),
463                        email: "john@techcorp.com".to_string(),
464                        phone: "+1-555-0123".to_string(),
465                    },
466                    Contact {
467                        contact_type: "secondary".to_string(),
468                        name: "Jane Smith".to_string(),
469                        email: "jane@techcorp.com".to_string(),
470                        phone: "+1-555-0124".to_string(),
471                    },
472                ],
473                location: Location {
474                    country: "USA".to_string(),
475                    city: "San Francisco".to_string(),
476                    timezone: "PST".to_string(),
477                    coordinates: Coordinates {
478                        format: "decimal".to_string(),
479                        latitude: 37.7749,
480                        longitude: -122.4194,
481                    },
482                },
483            },
484            Company {
485                id: 2,
486                company_type: "finance".to_string(),
487                name: "FinanceCo".to_string(),
488                founded_year: 2000,
489                active: true,
490                contact: vec![Contact {
491                    contact_type: "primary".to_string(),
492                    name: "Alice Brown".to_string(),
493                    email: "alice@financeco.com".to_string(),
494                    phone: "+1-555-0125".to_string(),
495                }],
496                location: Location {
497                    country: "UK".to_string(),
498                    city: "London".to_string(),
499                    timezone: "GMT".to_string(),
500                    coordinates: Coordinates {
501                        format: "decimal".to_string(),
502                        latitude: 51.5074,
503                        longitude: -0.1278,
504                    },
505                },
506            },
507        ];
508
509        writer.open().unwrap();
510        writer.write(&items).unwrap();
511        writer.close().unwrap();
512
513        // Read back the file to verify contents
514        let content = std::fs::read_to_string(temp_file.path()).unwrap();
515        println!("Generated XML:\n{}", content);
516
517        // Verify root structure
518        assert!(content.contains("<companies>"));
519        assert!(content.contains("</companies>"));
520
521        // Verify first company
522        assert!(content.contains("<company id=\"1\" type=\"tech\" active=\"true\">"));
523        assert!(content.contains("<name>TechCorp</name>"));
524        assert!(content.contains("<founded_year>2010</founded_year>"));
525
526        // Verify contacts
527        assert!(content.contains("<contact type=\"primary\">"));
528        assert!(content.contains("<name>John Doe</name>"));
529        assert!(content.contains("<email>john@techcorp.com</email>"));
530        assert!(content.contains("<phone>+1-555-0123</phone>"));
531        assert!(content.contains("<contact type=\"secondary\">"));
532        assert!(content.contains("<name>Jane Smith</name>"));
533
534        // Verify location
535        assert!(content.contains("<location country=\"USA\" timezone=\"PST\">"));
536        assert!(content.contains("<city>San Francisco</city>"));
537
538        // Verify coordinates
539        assert!(content.contains("<coordinates format=\"decimal\">"));
540        assert!(content.contains("<latitude>37.7749</latitude>"));
541        assert!(content.contains("<longitude>-122.4194</longitude>"));
542
543        // Verify second company
544        assert!(content.contains("<company id=\"2\" type=\"finance\" active=\"true\">"));
545        assert!(content.contains("<name>FinanceCo</name>"));
546        assert!(content.contains("<founded_year>2000</founded_year>"));
547        assert!(content.contains("<location country=\"UK\" timezone=\"GMT\">"));
548        assert!(content.contains("<city>London</city>"));
549    }
550
551    #[test]
552    fn test_in_memory_writing() {
553        let buffer = Cursor::new(Vec::new());
554        let writer = XmlItemWriterBuilder::<SimpleItem>::new()
555            .root_tag("items")
556            .item_tag("item")
557            .from_writer(buffer);
558
559        let items = vec![
560            SimpleItem {
561                id: 1,
562                name: "Item 1".to_string(),
563                value: 10.5,
564            },
565            SimpleItem {
566                id: 2,
567                name: "Item 2".to_string(),
568                value: 20.75,
569            },
570        ];
571
572        writer.open().unwrap();
573        writer.write(&items).unwrap();
574        writer.close().unwrap();
575
576        // Get the inner buffer from the writer
577        let content = {
578            let buf_writer = writer.writer.borrow_mut();
579            let cursor = buf_writer.get_ref().get_ref();
580            String::from_utf8(cursor.get_ref().clone()).unwrap()
581        };
582
583        assert!(content.contains("<items>"));
584        assert!(content.contains("<item>"));
585        assert!(content.contains("<id>1</id>"));
586        assert!(content.contains("<name>Item 1</name>"));
587        assert!(content.contains("<value>10.5</value>"));
588        assert!(content.contains("<id>2</id>"));
589        assert!(content.contains("<name>Item 2</name>"));
590        assert!(content.contains("<value>20.75</value>"));
591        assert!(content.contains("</item>"));
592        assert!(content.contains("</items>"));
593    }
594
595    #[test]
596    fn test_empty_collection() {
597        let buffer = Cursor::new(Vec::new());
598        let writer = XmlItemWriterBuilder::<SimpleItem>::new()
599            .root_tag("items")
600            .item_tag("item")
601            .from_writer(buffer);
602
603        let empty_items: Vec<SimpleItem> = vec![];
604
605        writer.open().unwrap();
606        writer.write(&empty_items).unwrap();
607        writer.close().unwrap();
608
609        // Get the inner buffer from the writer
610        let content = {
611            let buf_writer = writer.writer.borrow_mut();
612            let cursor = buf_writer.get_ref().get_ref();
613            String::from_utf8(cursor.get_ref().clone()).unwrap()
614        };
615
616        assert_eq!(content, "<items></items>");
617    }
618
619    #[test]
620    fn test_default_item_tag() {
621        let buffer = Cursor::new(Vec::new());
622
623        // Don't specify item_tag to test the default behavior
624        let writer = XmlItemWriterBuilder::<SimpleItem>::new()
625            .root_tag("items")
626            .from_writer(buffer);
627
628        let items = vec![SimpleItem {
629            id: 1,
630            name: "Test".to_string(),
631            value: 1.0,
632        }];
633
634        writer.open().unwrap();
635        writer.write(&items).unwrap();
636        writer.close().unwrap();
637
638        // Get the inner buffer from the writer
639        let content = {
640            let buf_writer = writer.writer.borrow_mut();
641            let cursor = buf_writer.get_ref().get_ref();
642            String::from_utf8(cursor.get_ref().clone()).unwrap()
643        };
644
645        // The default tag should be "simpleitem" (lowercase of SimpleItem)
646        assert!(content.contains("<simpleitem>"));
647        assert!(content.contains("</simpleitem>"));
648    }
649
650    #[test]
651    fn test_xml_escaping() {
652        let buffer = Cursor::new(Vec::new());
653        let writer = XmlItemWriterBuilder::<SimpleItem>::new()
654            .root_tag("items")
655            .item_tag("item")
656            .from_writer(buffer);
657
658        // Create items with special XML characters that need escaping
659        let items = vec![
660            SimpleItem {
661                id: 1,
662                name: "Item with < and > symbols".to_string(),
663                value: 10.5,
664            },
665            SimpleItem {
666                id: 2,
667                name: "Item with & and \" characters".to_string(),
668                value: 20.75,
669            },
670        ];
671
672        writer.open().unwrap();
673        writer.write(&items).unwrap();
674        writer.close().unwrap();
675
676        // Get the inner buffer from the writer
677        let content = {
678            let buf_writer = writer.writer.borrow_mut();
679            let cursor = buf_writer.get_ref().get_ref();
680            String::from_utf8(cursor.get_ref().clone()).unwrap()
681        };
682
683        // Print the content for debugging
684        println!("XML content: {}", content);
685
686        // Check that special characters are properly escaped
687        assert!(content.contains("Item with &lt; and &gt; symbols"));
688        // Use contains_any to check for either possible escaping format
689        assert!(content.contains("Item with &amp;") || content.contains("Item with &"));
690        assert!(content.contains("\"") || content.contains("&quot;"));
691    }
692
693    #[test]
694    fn test_array_fields() {
695        let buffer = Cursor::new(Vec::new());
696        let writer = XmlItemWriterBuilder::<Product>::new()
697            .root_tag("products")
698            .item_tag("product")
699            .from_writer(buffer);
700
701        let items = vec![
702            Product {
703                id: 1,
704                name: "Laptop".to_string(),
705                price: 999.99,
706                tags: vec![
707                    "electronics".to_string(),
708                    "computer".to_string(),
709                    "portable".to_string(),
710                ],
711            },
712            Product {
713                id: 2,
714                name: "Smartphone".to_string(),
715                price: 699.99,
716                tags: vec!["electronics".to_string(), "mobile".to_string()],
717            },
718        ];
719
720        writer.open().unwrap();
721        writer.write(&items).unwrap();
722        writer.close().unwrap();
723
724        // Get the inner buffer from the writer
725        let content = {
726            let buf_writer = writer.writer.borrow_mut();
727            let cursor = buf_writer.get_ref().get_ref();
728            String::from_utf8(cursor.get_ref().clone()).unwrap()
729        };
730
731        // Verify the array elements are properly serialized
732        assert!(content.contains("<products>"));
733        assert!(content.contains("<product>"));
734        assert!(content.contains("<id>1</id>"));
735        assert!(content.contains("<name>Laptop</name>"));
736        assert!(content.contains("<price>999.99</price>"));
737        assert!(content.contains("<tags>electronics</tags>"));
738        assert!(content.contains("<tags>computer</tags>"));
739        assert!(content.contains("<tags>portable</tags>"));
740        assert!(content.contains("<id>2</id>"));
741        assert!(content.contains("<name>Smartphone</name>"));
742        assert!(content.contains("<price>699.99</price>"));
743        assert!(content.contains("</product>"));
744        assert!(content.contains("</products>"));
745    }
746
747    #[test]
748    fn should_use_type_name_as_default_item_tag_when_not_set() {
749        let temp_file = NamedTempFile::new().unwrap();
750        // No .item_tag() call → tag derived from type name "simpleitem"
751        let writer = XmlItemWriterBuilder::<SimpleItem>::new()
752            .root_tag("items")
753            .from_path(temp_file.path())
754            .unwrap();
755        // item_tag should be the lowercase type-name suffix
756        assert_eq!(writer.item_tag, "simpleitem");
757    }
758
759    #[test]
760    fn should_return_error_when_flush_fails_on_io() {
761        struct FailWriter;
762        impl Write for FailWriter {
763            fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
764                Ok(buf.len()) // allow writes so open/close succeed
765            }
766            fn flush(&mut self) -> std::io::Result<()> {
767                Err(std::io::Error::new(
768                    std::io::ErrorKind::Other,
769                    "flush failed",
770                ))
771            }
772        }
773
774        let writer = XmlItemWriter::<SimpleItem, FailWriter> {
775            writer: RefCell::new(Writer::new(BufWriter::new(FailWriter))),
776            item_tag: "item".to_string(),
777            root_tag: "items".to_string(),
778            _phantom: PhantomData,
779        };
780
781        let result = writer.flush();
782        assert!(
783            result.is_err(),
784            "flush should fail when underlying writer fails"
785        );
786        match result.err().unwrap() {
787            BatchError::ItemWriter(msg) => {
788                assert!(
789                    msg.contains("flush"),
790                    "error message should mention flush, got: {msg}"
791                )
792            }
793            e => panic!("expected ItemWriter error, got {e:?}"),
794        }
795    }
796
797    #[test]
798    fn test_error_handling_invalid_path() {
799        // Try to create a writer with an invalid path
800        let invalid_path = "/nonexistent/directory/file.xml";
801        let result = XmlItemWriterBuilder::<SimpleItem>::new()
802            .root_tag("items")
803            .item_tag("item")
804            .from_path(invalid_path);
805
806        // Verify the result is an error
807        assert!(result.is_err());
808
809        // Verify the error message contains the expected information
810        if let Err(error) = result {
811            if let BatchError::ItemWriter(message) = error {
812                assert!(message.contains("Failed to create XML file"));
813            } else {
814                panic!("Expected ItemWriter error, got {:?}", error);
815            }
816        }
817    }
818}