pub mod common;
use std::{
env::temp_dir,
fs::{self, File, read_to_string},
io::Cursor,
};
use ::serde::{Deserialize, Serialize};
use rand::distr::{Alphanumeric, SampleString};
use spring_batch_rs::{
core::{
item::{ItemProcessor, ItemProcessorResult},
job::{Job, JobBuilder},
step::{StepBuilder, StepStatus},
},
item::{
csv::{csv_reader::CsvItemReaderBuilder, csv_writer::CsvItemWriterBuilder},
xml::{xml_reader::XmlItemReaderBuilder, xml_writer::XmlItemWriterBuilder},
},
};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
struct Product {
id: String,
name: String,
price: f64,
#[serde(default)]
description: Option<String>,
available: bool,
}
#[derive(Default)]
struct ProductProcessor;
impl ItemProcessor<Product, Product> for ProductProcessor {
fn process(&self, item: &Product) -> ItemProcessorResult<Product> {
let description = match &item.description {
Some(desc) => Some(desc.to_uppercase()),
None => Some("NO DESCRIPTION AVAILABLE".to_string()),
};
let product = Product {
id: item.id.clone(),
name: item.name.to_uppercase(),
price: item.price * 1.1, description,
available: item.available,
};
Ok(Some(product))
}
}
#[test]
fn transform_from_csv_file_to_csv_file_without_error() {
let csv_content = r#"id,name,price,description,available
P001,Wireless Headphones,79.99,"Noise-cancelling wireless headphones with 20hr battery life",true
P002,USB-C Cable,12.99,,false
P003,Smart Watch,149.99,"Fitness tracking smart watch with heart rate monitor",true"#;
let file_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let input_path = temp_dir().join(format!("{}.csv", file_name));
fs::write(&input_path, csv_content).expect("Failed to write CSV file");
let file = File::open(&input_path).expect("Unable to open CSV file");
let reader = CsvItemReaderBuilder::<Product>::new()
.has_headers(true)
.from_reader(file);
let processor = ProductProcessor;
let output_path = temp_dir().join(format!("output_{}.csv", file_name));
let writer = CsvItemWriterBuilder::new()
.has_headers(true)
.delimiter(b',')
.from_path(&output_path);
let step = StepBuilder::new("test")
.chunk::<Product, Product>(2)
.reader(&reader)
.processor(&processor)
.writer(&writer)
.build();
let job = JobBuilder::new().start(&step).build();
let result = job.run();
assert!(result.is_ok());
let csv_content =
read_to_string(&output_path).expect("Should have been able to read the CSV file");
assert!(!csv_content.is_empty());
assert!(csv_content.contains("WIRELESS HEADPHONES"));
assert!(csv_content.contains("USB-C CABLE"));
assert!(csv_content.contains("SMART WATCH"));
assert!(csv_content.contains("NOISE-CANCELLING WIRELESS HEADPHONES WITH 20HR BATTERY LIFE"));
assert!(csv_content.contains("NO DESCRIPTION AVAILABLE"));
fs::remove_file(&input_path).ok();
fs::remove_file(&output_path).ok();
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
#[allow(dead_code)]
struct EnhancedProduct {
id: String,
sku: String,
name: String,
price: f64,
manufacturer_country: String,
manufacturer_name: String,
manufacturer_founded_year: i32,
category1_name: String,
category1_main: bool,
category2_name: String,
category2_main: bool,
in_stock: bool,
}
#[test]
fn test_csv_reader_with_error_handling() {
let csv_content = r#"id,name,price,description,available
P001,Wireless Headphones,79.99,"Good headphones",true
P002,USB-C Cable,invalid_price,,false
P003,Smart Watch,149.99,"Fitness tracker",true"#;
let file_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let csv_path = temp_dir().join(format!("{}.csv", file_name));
fs::write(&csv_path, csv_content).expect("Failed to write CSV file");
let file = File::open(&csv_path).expect("Unable to open CSV file");
let reader = CsvItemReaderBuilder::<Product>::new()
.has_headers(true)
.from_reader(file);
let buffer = Cursor::new(Vec::new());
let writer = CsvItemWriterBuilder::<Product>::new()
.has_headers(true)
.from_writer(buffer);
let processor = ProductProcessor;
let step = StepBuilder::new("test")
.chunk::<Product, Product>(1)
.reader(&reader)
.processor(&processor)
.writer(&writer)
.skip_limit(1) .build();
let job = JobBuilder::new().start(&step).build();
let result = job.run();
assert!(result.is_ok());
let step_execution = job.get_step_execution("test").unwrap();
assert!(step_execution.status == StepStatus::Success);
assert!(step_execution.read_count == 2);
assert!(step_execution.write_count == 2);
assert!(step_execution.process_count == 2);
assert!(step_execution.read_error_count == 1);
assert!(step_execution.write_error_count == 0);
fs::remove_file(&csv_path).ok();
}
#[test]
fn test_csv_writer_with_custom_delimiter() {
let csv_content = r#"id,name,price,description,available
P001,Wireless Headphones,79.99,"Good headphones",true
P002,USB-C Cable,12.99,,false
P003,Smart Watch,149.99,"Fitness tracker",true"#;
let file_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let input_path = temp_dir().join(format!("{}.csv", file_name));
fs::write(&input_path, csv_content).expect("Failed to write CSV file");
let file = File::open(&input_path).expect("Unable to open CSV file");
let reader = CsvItemReaderBuilder::<Product>::new()
.has_headers(true)
.from_reader(file);
let processor = ProductProcessor;
let output_path = temp_dir().join(format!("output_{}.csv", file_name));
let writer = CsvItemWriterBuilder::<Product>::new()
.has_headers(true)
.delimiter(b';')
.from_path(&output_path);
let step = StepBuilder::new("test")
.chunk::<Product, Product>(2)
.reader(&reader)
.processor(&processor)
.writer(&writer)
.build();
let job = JobBuilder::new().start(&step).build();
let result = job.run();
assert!(result.is_ok());
let step_execution = job.get_step_execution("test").unwrap();
assert!(step_execution.status == StepStatus::Success);
assert!(step_execution.read_count == 3);
assert!(step_execution.write_count == 3);
assert!(step_execution.process_count == 3);
assert!(step_execution.read_error_count == 0);
assert!(step_execution.write_error_count == 0);
let csv_content =
read_to_string(&output_path).expect("Should have been able to read the CSV file");
assert!(csv_content.contains(";"));
assert!(!csv_content.contains(","));
fs::remove_file(&input_path).ok();
fs::remove_file(&output_path).ok();
}
#[test]
fn transform_from_csv_to_xml() {
let csv_content = r#"id,name,price,description,available
P001,Wireless Headphones,79.99,"Noise-cancelling wireless headphones with 20hr battery life",true
P002,USB-C Cable,12.99,,false
P003,Smart Watch,149.99,"Fitness tracking smart watch with heart rate monitor",true"#;
let file_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let input_path = temp_dir().join(format!("{}.csv", file_name));
fs::write(&input_path, csv_content).expect("Failed to write CSV file");
let file = File::open(&input_path).expect("Unable to open CSV file");
let reader = CsvItemReaderBuilder::<Product>::new()
.has_headers(true)
.from_reader(file);
let processor = ProductProcessor;
let output_path = temp_dir().join(format!("output_{}.xml", file_name));
let writer = XmlItemWriterBuilder::<Product>::new()
.root_tag("products")
.item_tag("product")
.from_path(&output_path)
.expect("Failed to create XML writer");
let step = StepBuilder::new("test")
.chunk(2)
.reader(&reader)
.processor(&processor)
.writer(&writer)
.build();
let job = JobBuilder::new().start(&step).build();
let result = job.run();
assert!(result.is_ok());
let xml_content =
read_to_string(&output_path).expect("Should have been able to read the XML file");
assert!(xml_content.contains("<products>"));
assert!(xml_content.contains("<product>"));
assert!(xml_content.contains("WIRELESS HEADPHONES"));
assert!(xml_content.contains("USB-C CABLE"));
assert!(xml_content.contains("SMART WATCH"));
assert!(xml_content.contains("NOISE-CANCELLING WIRELESS HEADPHONES WITH 20HR BATTERY LIFE"));
assert!(xml_content.contains("NO DESCRIPTION AVAILABLE"));
fs::remove_file(&input_path).ok();
fs::remove_file(&output_path).ok();
}
#[test]
fn transform_from_xml_to_csv() {
let xml_content = r#"
<products>
<product>
<id>P001</id>
<name>Wireless Headphones</name>
<price>79.99</price>
<description>Noise-cancelling wireless headphones with 20hr battery life</description>
<available>true</available>
</product>
<product>
<id>P002</id>
<name>USB-C Cable</name>
<price>12.99</price>
<available>false</available>
</product>
<product>
<id>P003</id>
<name>Smart Watch</name>
<price>149.99</price>
<description>Fitness tracking smart watch with heart rate monitor</description>
<available>true</available>
</product>
</products>
"#;
let file_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let input_path = temp_dir().join(format!("{}.xml", file_name));
fs::write(&input_path, xml_content).expect("Failed to write XML file");
let file = File::open(&input_path).expect("Unable to open XML file");
let reader = XmlItemReaderBuilder::<Product>::new()
.tag("product")
.from_reader(file);
let processor = ProductProcessor;
let output_path = temp_dir().join(format!("output_{}.csv", file_name));
let writer = CsvItemWriterBuilder::<Product>::new()
.has_headers(true)
.delimiter(b',')
.from_path(&output_path);
let step = StepBuilder::new("test")
.chunk(2)
.reader(&reader)
.processor(&processor)
.writer(&writer)
.build();
let job = JobBuilder::new().start(&step).build();
let result = job.run();
assert!(result.is_ok());
let csv_content =
read_to_string(&output_path).expect("Should have been able to read the CSV file");
assert!(csv_content.contains("id,name,price,description,available"));
assert!(csv_content.contains("WIRELESS HEADPHONES"));
assert!(csv_content.contains("USB-C CABLE"));
assert!(csv_content.contains("SMART WATCH"));
assert!(csv_content.contains("NOISE-CANCELLING WIRELESS HEADPHONES WITH 20HR BATTERY LIFE"));
assert!(csv_content.contains("NO DESCRIPTION AVAILABLE"));
fs::remove_file(&input_path).ok();
fs::remove_file(&output_path).ok();
}
#[test]
fn test_bidirectional_transformation() {
let csv_content = r#"id,name,price,description,available
P001,Wireless Headphones,79.99,"Noise-cancelling wireless headphones with 20hr battery life",true
P002,USB-C Cable,12.99,,false
P003,Smart Watch,149.99,"Fitness tracking smart watch with heart rate monitor",true"#;
let file_name = Alphanumeric.sample_string(&mut rand::rng(), 16);
let csv_path = temp_dir().join(format!("{}.csv", file_name));
fs::write(&csv_path, csv_content).expect("Failed to write CSV file");
let file = File::open(&csv_path).expect("Unable to open CSV file");
let reader = CsvItemReaderBuilder::<Product>::new()
.has_headers(true)
.from_reader(file);
let xml_path = temp_dir().join(format!("intermediate_{}.xml", file_name));
let writer = XmlItemWriterBuilder::<Product>::new()
.root_tag("products")
.item_tag("product")
.from_path(&xml_path)
.expect("Failed to create XML writer");
let processor1 = ProductProcessor;
let step1 = StepBuilder::new("test")
.chunk::<Product, Product>(2)
.reader(&reader)
.processor(&processor1)
.writer(&writer)
.build();
let job1 = JobBuilder::new().start(&step1).build();
let result1 = job1.run();
assert!(result1.is_ok());
let file = File::open(&xml_path).expect("Unable to open XML file");
let reader = XmlItemReaderBuilder::<Product>::new()
.tag("product")
.from_reader(file);
let final_csv_path = temp_dir().join(format!("final_{}.csv", file_name));
let writer = CsvItemWriterBuilder::new()
.has_headers(true)
.delimiter(b',')
.from_path(&final_csv_path);
let processor2 = ProductProcessor;
let step2 = StepBuilder::new("test")
.chunk::<Product, Product>(2)
.reader(&reader)
.processor(&processor2)
.writer(&writer)
.build();
let job2 = JobBuilder::new().start(&step2).build();
let result2 = job2.run();
assert!(result2.is_ok());
let final_csv_content =
read_to_string(&final_csv_path).expect("Should have been able to read the final CSV file");
assert!(final_csv_content.contains("WIRELESS HEADPHONES"));
assert!(final_csv_content.contains("USB-C CABLE"));
assert!(final_csv_content.contains("SMART WATCH"));
assert!(
final_csv_content.contains("NOISE-CANCELLING WIRELESS HEADPHONES WITH 20HR BATTERY LIFE")
);
assert!(final_csv_content.contains("NO DESCRIPTION AVAILABLE"));
fs::remove_file(&csv_path).ok();
fs::remove_file(&xml_path).ok();
fs::remove_file(&final_csv_path).ok();
}