mod error;
use std::io::{Cursor, Read};
use quick_xml::{
de::from_str,
events::{BytesEnd, BytesStart, BytesText, Event},
Reader, Writer,
};
use serde::Deserialize;
use crate::error::InvalidArgumentError;
use crate::protocol::{
product::{
payload::{
ProductCreateAction, ProductCreateActionBuilder, ProductUpdateAction,
ProductUpdateActionBuilder,
},
state::ProductNamespace,
},
schema::state::{DataType, PropertyValueBuilder},
};
pub use error::ProductGdsnError;
pub const GDSN_3_1_PROPERTY_NAME: &str = "GDSN_3_1";
#[derive(Debug, Deserialize, PartialEq)]
pub struct GridTradeItems {
#[serde(rename = "tradeItem")]
pub trade_items: Vec<TradeItem>,
}
#[derive(Debug, Deserialize, PartialEq)]
pub struct TradeItem {
pub gtin: String,
#[serde(default)]
pub payload: String,
}
impl TradeItem {
pub fn into_create_payload(self, owner: &str) -> Result<ProductCreateAction, ProductGdsnError> {
let xml_property = PropertyValueBuilder::new()
.with_name(GDSN_3_1_PROPERTY_NAME.to_string())
.with_data_type(DataType::String)
.with_string_value(self.payload)
.build()?;
Ok(ProductCreateActionBuilder::new()
.with_product_id(self.gtin)
.with_product_namespace(ProductNamespace::Gs1)
.with_owner(owner.to_string())
.with_properties(vec![xml_property])
.build()?)
}
pub fn into_update_payload(self) -> Result<ProductUpdateAction, ProductGdsnError> {
let gdsn_property = PropertyValueBuilder::new()
.with_name(GDSN_3_1_PROPERTY_NAME.to_string())
.with_data_type(DataType::String)
.with_string_value(self.payload)
.build()?;
Ok(ProductUpdateActionBuilder::new()
.with_product_id(self.gtin)
.with_product_namespace(ProductNamespace::Gs1)
.with_properties(vec![gdsn_property])
.build()?)
}
}
pub fn get_trade_items_from_xml(path: &str) -> Result<Vec<TradeItem>, ProductGdsnError> {
let mut xml_file = std::fs::File::open(path).map_err(|error| {
ProductGdsnError::InvalidArgument(InvalidArgumentError::new(
path.to_string(),
error.to_string(),
))
})?;
let mut xml_str = String::new();
xml_file.read_to_string(&mut xml_str).map_err(|error| {
ProductGdsnError::InvalidArgument(InvalidArgumentError::new(
path.to_string(),
error.to_string(),
))
})?;
let mut grid_trade_items: GridTradeItems = from_str(&xml_str).map_err(|error| {
ProductGdsnError::InvalidArgument(InvalidArgumentError::new(
path.to_string(),
error.to_string(),
))
})?;
let mut reader = Reader::from_str(&xml_str);
reader.trim_text(true);
let mut buf = Vec::new();
loop {
match reader.read_event(&mut buf) {
Ok(Event::Start(ref e)) if e.name() == b"tradeItem" => {
let mut writer = Writer::new(Cursor::new(Vec::new()));
let mut elem = BytesStart::owned(b"tradeItem".to_vec(), "tradeItem".len());
elem.extend_attributes(e.attributes().map(|attr| attr.unwrap()));
writer.write_event(Event::Start(elem))?;
let mut gtin_buf = Vec::new();
let mut current_gtin = String::new();
loop {
match reader.read_event(&mut buf) {
Ok(Event::End(ref e)) if e.name() == b"tradeItem" => {
writer.write_event(Event::End(BytesEnd::borrowed(b"tradeItem")))?;
let result_bytes = writer.into_inner().into_inner();
let result = std::str::from_utf8(&result_bytes)?;
for trade_item in grid_trade_items.trade_items.iter_mut() {
if trade_item.gtin == current_gtin {
trade_item.payload = result.to_string();
}
}
break;
}
Ok(Event::Start(ref e)) if e.name() == b"gtin" => {
reader.read_text(&e.name(), &mut gtin_buf)?;
current_gtin = std::str::from_utf8(>in_buf)?
.split('/')
.next()
.ok_or_else(|| {
ProductGdsnError::InvalidArgument(InvalidArgumentError::new(
path.to_string(),
"Unable to parse GTIN from XML".to_string(),
))
})?
.to_string();
writer.write_event(Event::Start(BytesStart::owned(
b"gtin".to_vec(),
"gtin".len(),
)))?;
writer.write_event(Event::Text(BytesText::from_plain(
current_gtin.as_bytes(),
)))?;
writer.write_event(Event::End(BytesEnd::owned(b"gtin".to_vec())))?;
}
Ok(Event::Eof) => break,
Ok(e) => writer.write_event(&e)?,
Err(e) => {
return Err(ProductGdsnError::InvalidArgument(
InvalidArgumentError::new(
path.to_string(),
format!(
"Error at position {}: {:?}",
reader.buffer_position(),
e
),
),
));
}
}
}
}
Ok(Event::Eof) => break,
Err(e) => {
return Err(ProductGdsnError::InvalidArgument(
InvalidArgumentError::new(
path.to_string(),
format!("Error at position {}: {:?}", reader.buffer_position(), e),
),
));
}
_ => (),
}
buf.clear();
}
Ok(grid_trade_items.trade_items)
}
#[cfg(test)]
mod tests {
use super::*;
use std::{fs::read_to_string, path::PathBuf};
use crate::error::InvalidArgumentError;
const TEST_GTIN_1: &str = "00734730437958";
const TEST_GTIN_2: &str = "10036016500279";
fn get_expected_trade_item_payload_from_file(path: &str) -> String {
let mut payload_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
payload_path.push(path);
let payload = read_to_string(payload_path)
.unwrap()
.replace(" ", "")
.replace("\n", "");
payload
}
#[test]
fn test_get_trade_item_from_xml_success() {
let mut test_gdsn_product_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_gdsn_product_path.push("src/product/gdsn/test_files/gdsn_product.xml");
let path_str = test_gdsn_product_path.to_str().unwrap();
let result = get_trade_items_from_xml(path_str);
assert!(result.is_ok());
let expected = TradeItem {
gtin: TEST_GTIN_1.to_string(),
payload: get_expected_trade_item_payload_from_file(
"src/product/gdsn/test_files/test_trade_item_payload_1.xml",
),
};
assert_eq!(result.unwrap(), vec![expected]);
}
#[test]
fn test_get_multiple_trade_items_from_xml_success() {
let mut test_gdsn_product_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_gdsn_product_path.push("src/product/gdsn/test_files/gdsn_product_multiple.xml");
let path_str = test_gdsn_product_path.to_str().unwrap();
let result = get_trade_items_from_xml(path_str);
assert!(result.is_ok());
let expected_1 = TradeItem {
gtin: TEST_GTIN_1.to_string(),
payload: get_expected_trade_item_payload_from_file(
"src/product/gdsn/test_files/test_trade_item_payload_1.xml",
),
};
let expected_2 = TradeItem {
gtin: TEST_GTIN_2.to_string(),
payload: get_expected_trade_item_payload_from_file(
"src/product/gdsn/test_files/test_trade_item_payload_2.xml",
),
};
assert_eq!(result.unwrap(), vec![expected_1, expected_2]);
}
#[test]
fn test_get_trade_items_from_xml_poorly_formed() {
let mut test_gdsn_product_path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_gdsn_product_path.push("src/product/gdsn/test_files/gdsn_product_poorly_formed.xml");
let path_str = test_gdsn_product_path.to_str().unwrap();
let result = get_trade_items_from_xml(path_str);
assert!(result.is_err());
let expected_error = InvalidArgumentError::new(
path_str.to_string(),
"Expecting </tradeItem> found </gridTradeItems>".to_string(),
);
assert_eq!(result.unwrap_err().to_string(), expected_error.to_string());
}
}