spring_batch_rs/item/json/
json_writer.rs1use std::{
2 cell::{Cell, RefCell},
3 fs::File,
4 io::{BufWriter, Write},
5 marker::PhantomData,
6 path::Path,
7};
8
9use crate::{
10 BatchError,
11 core::item::{ItemWriter, ItemWriterResult},
12};
13
14pub struct JsonItemWriter<O, W: Write> {
54 stream: RefCell<BufWriter<W>>,
56 use_pretty_formatter: bool,
58 indent: Box<[u8]>,
60 is_first_element: Cell<bool>,
62 _phantom: PhantomData<O>,
63}
64
65impl<O: serde::Serialize, W: Write> ItemWriter<O> for JsonItemWriter<O, W> {
66 fn write(&self, items: &[O]) -> ItemWriterResult {
79 let mut json_chunk = String::new();
80
81 for item in items.iter() {
82 if !self.is_first_element.get() {
83 json_chunk.push(',');
84 } else {
85 self.is_first_element.set(false);
86 }
87
88 let result = if self.use_pretty_formatter {
89 let mut buf = Vec::new();
91 let formatter = serde_json::ser::PrettyFormatter::with_indent(&self.indent);
92 let mut ser = serde_json::Serializer::with_formatter(&mut buf, formatter);
93 match item.serialize(&mut ser) {
94 Ok(_) => match String::from_utf8(buf) {
95 Ok(s) => Ok(s),
96 Err(e) => Err(BatchError::ItemWriter(e.to_string())),
97 },
98 Err(e) => Err(BatchError::ItemWriter(e.to_string())),
99 }
100 } else {
101 serde_json::to_string(item).map_err(|e| BatchError::ItemWriter(e.to_string()))
102 };
103
104 match result {
105 Ok(json_str) => json_chunk.push_str(&json_str),
106 Err(e) => return Err(e),
107 }
108
109 if self.use_pretty_formatter {
110 json_chunk.push('\n');
111 }
112 }
113
114 let result = self.stream.borrow_mut().write_all(json_chunk.as_bytes());
115
116 match result {
117 Ok(_ser) => Ok(()),
118 Err(error) => Err(BatchError::ItemWriter(error.to_string())),
119 }
120 }
121
122 fn flush(&self) -> ItemWriterResult {
128 let result = self.stream.borrow_mut().flush();
129
130 match result {
131 Ok(()) => Ok(()),
132 Err(error) => Err(BatchError::ItemWriter(error.to_string())),
133 }
134 }
135
136 fn open(&self) -> ItemWriterResult {
144 let begin_array = if self.use_pretty_formatter {
145 b"[\n".to_vec()
146 } else {
147 b"[".to_vec()
148 };
149
150 let result = self.stream.borrow_mut().write_all(&begin_array);
151
152 match result {
153 Ok(()) => Ok(()),
154 Err(error) => Err(BatchError::ItemWriter(error.to_string())),
155 }
156 }
157
158 fn close(&self) -> ItemWriterResult {
167 let end_array = if self.use_pretty_formatter {
168 b"\n]\n".to_vec()
169 } else {
170 b"]\n".to_vec()
171 };
172
173 let result = self.stream.borrow_mut().write_all(&end_array);
174 let _ = self.stream.borrow_mut().flush();
175
176 match result {
177 Ok(()) => Ok(()),
178 Err(error) => Err(BatchError::ItemWriter(error.to_string())),
179 }
180 }
181}
182
183pub struct JsonItemWriterBuilder<O> {
223 indent: Box<[u8]>,
225 pretty_formatter: bool,
227 _pd: PhantomData<O>,
229}
230
231impl<O> Default for JsonItemWriterBuilder<O> {
232 fn default() -> Self {
233 Self::new()
234 }
235}
236
237impl<O> JsonItemWriterBuilder<O> {
238 pub fn new() -> Self {
251 Self {
252 indent: Box::from(b" ".to_vec()),
253 pretty_formatter: false,
254 _pd: PhantomData,
255 }
256 }
257
258 pub fn indent(mut self, indent: &[u8]) -> Self {
273 self.indent = Box::from(indent);
274 self
275 }
276
277 pub fn pretty_formatter(mut self, yes: bool) -> Self {
297 self.pretty_formatter = yes;
298 self
299 }
300
301 pub fn from_path<W: AsRef<Path>>(self, path: W) -> JsonItemWriter<O, File> {
347 let file = File::create(path).expect("Unable to open file");
348
349 let buf_writer = BufWriter::new(file);
350
351 JsonItemWriter {
352 stream: RefCell::new(buf_writer),
353 use_pretty_formatter: self.pretty_formatter,
354 indent: self.indent.clone(),
355 is_first_element: Cell::new(true),
356 _phantom: PhantomData,
357 }
358 }
359
360 pub fn from_writer<W: Write>(self, wtr: W) -> JsonItemWriter<O, W> {
402 let buf_writer = BufWriter::new(wtr);
403
404 JsonItemWriter {
405 stream: RefCell::new(buf_writer),
406 use_pretty_formatter: self.pretty_formatter,
407 indent: self.indent,
408 is_first_element: Cell::new(true),
409 _phantom: PhantomData,
410 }
411 }
412}
413
414#[cfg(test)]
415mod tests {
416 use super::*;
417 use crate::core::item::ItemWriter;
418 use serde::Serialize;
419 use std::fs;
420 use tempfile::tempdir;
421
422 #[derive(Serialize, Debug, PartialEq)]
423 struct TestItem {
424 id: u32,
425 name: String,
426 value: f64,
427 }
428
429 #[test]
430 fn json_writer_builder_should_create_with_defaults() {
431 let builder = JsonItemWriterBuilder::<TestItem>::new();
432 assert!(!builder.pretty_formatter);
433 assert_eq!(builder.indent, b" ".to_vec().into_boxed_slice());
434 }
435
436 #[test]
437 fn json_writer_builder_should_set_pretty_formatter() {
438 let builder = JsonItemWriterBuilder::<TestItem>::new().pretty_formatter(true);
439 assert!(builder.pretty_formatter);
440 }
441
442 #[test]
443 fn json_writer_builder_should_set_custom_indent() {
444 let custom_indent = b" ";
445 let builder = JsonItemWriterBuilder::<TestItem>::new().indent(custom_indent);
446 assert_eq!(builder.indent, custom_indent.to_vec().into_boxed_slice());
447 }
448
449 #[test]
450 fn json_writer_builder_should_implement_default() {
451 let builder1 = JsonItemWriterBuilder::<TestItem>::new();
452 let builder2 = JsonItemWriterBuilder::<TestItem>::default();
453
454 assert_eq!(builder1.pretty_formatter, builder2.pretty_formatter);
455 assert_eq!(builder1.indent, builder2.indent);
457 }
458
459 #[test]
460 fn json_writer_builder_should_support_generic_type() {
461 let _builder = JsonItemWriterBuilder::<TestItem>::new();
462 let _builder_default = JsonItemWriterBuilder::<TestItem>::default();
463 }
464
465 #[test]
466 fn json_writer_from_path_should_create_file_writer() {
467 let temp_dir = tempdir().unwrap();
468 let file_path = temp_dir.path().join("test_output.json");
469
470 let writer: JsonItemWriter<TestItem, File> =
471 JsonItemWriterBuilder::new().from_path(&file_path);
472
473 let item = TestItem {
474 id: 1,
475 name: "test".to_string(),
476 value: 42.5,
477 };
478
479 writer.open().unwrap();
480 writer.write(&[item]).unwrap();
481 writer.close().unwrap();
482
483 let content = fs::read_to_string(&file_path).unwrap();
485 assert!(content.contains(r#"{"id":1,"name":"test","value":42.5}"#));
486 }
487
488 #[test]
489 fn json_writer_should_handle_custom_indent() {
490 let temp_dir = tempdir().unwrap();
491 let file_path = temp_dir.path().join("indent_test.json");
492
493 let writer = JsonItemWriterBuilder::new()
494 .pretty_formatter(true)
495 .indent(b"\t")
496 .from_path(&file_path);
497
498 let item = TestItem {
499 id: 1,
500 name: "test".to_string(),
501 value: 42.5,
502 };
503
504 writer.open().unwrap();
505 writer.write(&[item]).unwrap();
506 writer.close().unwrap();
507
508 let content = fs::read_to_string(&file_path).unwrap();
509 assert!(content.contains('\t'));
512 }
513
514 #[test]
515 fn json_writer_should_handle_pretty_formatting() {
516 let temp_dir = tempdir().unwrap();
517 let file_path = temp_dir.path().join("pretty_test.json");
518
519 let writer = JsonItemWriterBuilder::new()
520 .pretty_formatter(true)
521 .from_path(&file_path);
522
523 let item = TestItem {
524 id: 1,
525 name: "test".to_string(),
526 value: 42.5,
527 };
528
529 writer.open().unwrap();
530 writer.write(&[item]).unwrap();
531 writer.close().unwrap();
532
533 let content = fs::read_to_string(&file_path).unwrap();
534 assert!(content.contains("[\n"));
535 assert!(content.contains("\n]\n"));
536 assert!(content.contains(" \"id\": 1"));
537 }
538
539 #[test]
540 fn json_writer_should_handle_empty_items() {
541 let temp_dir = tempdir().unwrap();
542 let file_path = temp_dir.path().join("empty_test.json");
543
544 let writer = JsonItemWriterBuilder::new().from_path(&file_path);
545 let empty_items: Vec<TestItem> = vec![];
546
547 writer.open().unwrap();
548 writer.write(&empty_items).unwrap();
549 writer.close().unwrap();
550
551 let content = fs::read_to_string(&file_path).unwrap();
552 assert_eq!(content, "[]\n");
553 }
554
555 #[test]
556 fn json_writer_should_handle_multiple_writes() {
557 let temp_dir = tempdir().unwrap();
558 let file_path = temp_dir.path().join("multi_test.json");
559
560 let writer = JsonItemWriterBuilder::new().from_path(&file_path);
561
562 let item1 = TestItem {
563 id: 1,
564 name: "first".to_string(),
565 value: 10.0,
566 };
567 let item2 = TestItem {
568 id: 2,
569 name: "second".to_string(),
570 value: 20.0,
571 };
572
573 writer.open().unwrap();
574 writer.write(&[item1]).unwrap();
575 writer.write(&[item2]).unwrap();
576 writer.close().unwrap();
577
578 let content = fs::read_to_string(&file_path).unwrap();
579 assert!(content.contains(r#"{"id":1,"name":"first","value":10.0}"#));
580 assert!(content.contains(r#"{"id":2,"name":"second","value":20.0}"#));
581 assert!(content.contains(','));
582 }
583
584 #[test]
585 fn json_writer_should_write_to_in_memory_buffer() {
586 use std::io::Cursor;
587
588 let buf = Cursor::new(Vec::new());
589 let writer = JsonItemWriterBuilder::<TestItem>::new().from_writer(buf);
590
591 let item = TestItem {
592 id: 7,
593 name: "cursor".to_string(),
594 value: 0.5,
595 };
596 writer.open().unwrap();
597 writer.write(&[item]).unwrap();
598 writer.close().unwrap();
599 }
601
602 #[test]
603 fn json_writer_should_flush_without_error() {
604 let temp_dir = tempdir().unwrap();
605 let file_path = temp_dir.path().join("flush_test.json");
606
607 let writer = JsonItemWriterBuilder::<TestItem>::new().from_path(&file_path);
608 writer.open().unwrap();
609 writer.flush().unwrap();
610 writer.close().unwrap();
611 }
612
613 #[test]
614 fn json_writer_compact_open_writes_bracket_without_newline() {
615 let temp_dir = tempdir().unwrap();
616 let file_path = temp_dir.path().join("compact_open.json");
617
618 let writer = JsonItemWriterBuilder::<TestItem>::new()
619 .pretty_formatter(false)
620 .from_path(&file_path);
621 writer.open().unwrap();
622 writer.close().unwrap();
623
624 let content = fs::read_to_string(&file_path).unwrap();
625 assert_eq!(
626 content, "[]\n",
627 "compact format should produce []\\n, got: {content:?}"
628 );
629 }
630
631 struct FailWriter;
633 impl std::io::Write for FailWriter {
634 fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
635 Err(std::io::Error::new(
636 std::io::ErrorKind::Other,
637 "write failed",
638 ))
639 }
640 fn flush(&mut self) -> std::io::Result<()> {
641 Err(std::io::Error::new(
642 std::io::ErrorKind::Other,
643 "flush failed",
644 ))
645 }
646 }
647
648 fn fail_json_writer<O: Serialize>() -> JsonItemWriter<O, FailWriter> {
649 JsonItemWriter {
650 stream: RefCell::new(BufWriter::with_capacity(0, FailWriter)),
651 use_pretty_formatter: false,
652 indent: Box::from(b" ".as_slice()),
653 is_first_element: Cell::new(true),
654 _phantom: PhantomData,
655 }
656 }
657
658 #[test]
659 fn should_return_error_when_open_fails_on_io() {
660 let writer = fail_json_writer::<String>();
661 let result = ((&writer) as &dyn ItemWriter<String>).open();
662 assert!(result.is_err(), "open should fail when writer fails");
663 }
664
665 #[test]
666 fn should_return_error_when_close_fails_on_io() {
667 let writer = fail_json_writer::<String>();
668 let result = ((&writer) as &dyn ItemWriter<String>).close();
669 assert!(result.is_err(), "close should fail when writer fails");
670 }
671
672 #[test]
673 fn should_return_error_when_flush_fails_on_io() {
674 let writer = fail_json_writer::<String>();
675 let result = ((&writer) as &dyn ItemWriter<String>).flush();
676 assert!(result.is_err(), "flush should fail when writer fails");
677 }
678
679 #[test]
680 fn should_return_error_when_write_fails_on_io() {
681 let writer = fail_json_writer::<String>();
682 let result = ((&writer) as &dyn ItemWriter<String>).write(&["hello".to_string()]);
685 assert!(
686 result.is_err(),
687 "write should fail when underlying IO fails"
688 );
689 }
690
691 #[test]
692 fn should_return_error_when_serialization_fails_with_pretty_formatter() {
693 use crate::BatchError;
694
695 struct NonSerializable;
696 impl Serialize for NonSerializable {
697 fn serialize<S: serde::Serializer>(&self, _s: S) -> Result<S::Ok, S::Error> {
698 Err(serde::ser::Error::custom(
699 "intentional serialization failure",
700 ))
701 }
702 }
703
704 let buf = std::io::Cursor::new(Vec::new());
705 let writer = JsonItemWriterBuilder::<NonSerializable>::new()
706 .pretty_formatter(true)
707 .from_writer(buf);
708 let result = ((&writer) as &dyn ItemWriter<NonSerializable>).write(&[NonSerializable]);
709 match result.err().unwrap() {
710 BatchError::ItemWriter(msg) => assert!(
711 msg.contains("intentional"),
712 "error should contain serialization message, got: {msg}"
713 ),
714 e => panic!("expected ItemWriter error, got {e:?}"),
715 }
716 }
717}