#![deny(missing_docs)]
#![deny(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use crate::events::EPCISEvent;
pub fn deserialize_string_or_map_text<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: serde::Deserializer<'de>,
{
struct StringVisitor;
impl<'de> serde::de::Visitor<'de> for StringVisitor {
type Value = String;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a string or a map with a text value")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v.to_string())
}
fn visit_string<E>(self, v: String) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(v)
}
fn visit_map<A>(self, mut map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let mut value: Option<String> = None;
while let Some(key) = map.next_key::<String>()? {
if key == "$text" || key == "$value" || key == "" {
value = Some(map.next_value()?);
} else {
let _: serde::de::IgnoredAny = map.next_value()?;
}
}
value.ok_or_else(|| serde::de::Error::custom("missing string value"))
}
}
deserializer.deserialize_any(StringVisitor)
}
pub mod datetime_serde {
use chrono::{DateTime, Utc};
use serde::{self, Serializer};
pub fn serialize<S>(date: &DateTime<Utc>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&date.to_rfc3339())
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<DateTime<Utc>, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = super::deserialize_string_or_map_text(deserializer)?;
s.parse::<DateTime<Utc>>().map_err(serde::de::Error::custom)
}
}
pub mod opt_datetime_serde {
use chrono::{DateTime, Utc};
use serde::{self, Serializer};
pub fn serialize<S>(date: &Option<DateTime<Utc>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
match date {
Some(d) => serializer.serialize_some(&d.to_rfc3339()),
None => serializer.serialize_none(),
}
}
pub fn deserialize<'de, D>(deserializer: D) -> Result<Option<DateTime<Utc>>, D::Error>
where
D: serde::Deserializer<'de>,
{
struct OptDateTimeVisitor;
impl<'de> serde::de::Visitor<'de> for OptDateTimeVisitor {
type Value = Option<DateTime<Utc>>;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("an optional datetime string")
}
fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
fn visit_some<D2>(self, deserializer: D2) -> Result<Self::Value, D2::Error>
where
D2: serde::Deserializer<'de>,
{
let s = super::deserialize_string_or_map_text(deserializer)?;
if s.is_empty() {
Ok(None)
} else {
s.parse::<DateTime<Utc>>().map(Some).map_err(serde::de::Error::custom)
}
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
if v.is_empty() {
Ok(None)
} else {
v.parse::<DateTime<Utc>>().map(Some).map_err(serde::de::Error::custom)
}
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
let s = super::deserialize_string_or_map_text(serde::de::value::MapAccessDeserializer::new(map))?;
if s.is_empty() {
Ok(None)
} else {
s.parse::<DateTime<Utc>>().map(Some).map_err(serde::de::Error::custom)
}
}
}
deserializer.deserialize_any(OptDateTimeVisitor)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EPCISDocument {
#[serde(rename = "@context")]
pub context: serde_json::Value,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "type")]
pub r#type: String,
pub schema_version: String,
#[serde(with = "crate::document::datetime_serde")]
pub creation_date: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub epcis_header: Option<EPCISHeader>,
pub epcis_body: EPCISBody,
#[serde(flatten)]
pub extensions: serde_json::Map<String, serde_json::Value>,
}
impl EPCISDocument {
#[must_use]
pub fn new(event_list: Vec<EPCISEvent>) -> Self {
Self {
context: serde_json::json!([
"https://ref.gs1.org/standards/epcis/2.0.0/epcis-context.jsonld"
]),
id: None,
r#type: "EPCISDocument".to_string(),
schema_version: "2.0".to_string(),
creation_date: Utc::now(),
epcis_header: None,
epcis_body: EPCISBody { event_list },
extensions: serde_json::Map::new(),
}
}
pub fn from_xml(xml_str: &str) -> Result<Self, quick_xml::DeError> {
let raw_val: serde_json::Value = quick_xml::de::from_str(xml_str)?;
let clean_val = clean_json_value(raw_val);
serde_json::from_value(clean_val).map_err(|e| {
quick_xml::DeError::Custom(e.to_string())
})
}
pub fn to_xml(&self) -> Result<String, quick_xml::se::SeError> {
quick_xml::se::to_string_with_root("EPCISDocument", self)
}
}
fn is_array_field(key: &str) -> bool {
matches!(
key,
"eventList"
| "epcList"
| "quantityList"
| "sensorElementList"
| "sensorReport"
| "sourceList"
| "destinationList"
| "bizTransactionList"
)
}
fn clean_json_value(v: serde_json::Value) -> serde_json::Value {
match v {
serde_json::Value::Object(mut map) => {
if map.len() == 1 {
if let Some(val) = map.remove("$text").or_else(|| map.remove("$value")) {
return clean_json_value(val);
}
}
let cleaned = map
.into_iter()
.map(|(k, val)| {
let cleaned_val = clean_json_value(val);
if is_array_field(&k) {
match cleaned_val {
serde_json::Value::Array(_) => (k, cleaned_val),
other => (k, serde_json::Value::Array(vec![other])),
}
} else {
(k, cleaned_val)
}
})
.collect();
serde_json::Value::Object(cleaned)
}
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.into_iter().map(clean_json_value).collect())
}
other => other,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EPCISHeader {
#[serde(skip_serializing_if = "Option::is_none")]
pub epcis_master_data: Option<EPCISMasterData>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EPCISMasterData {
pub vocabulary_list: Vec<VocabularyElement>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VocabularyElement {
#[serde(rename = "type")]
pub r#type: String,
pub element_list: Vec<VocabularyElementList>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VocabularyElementList {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<Vec<VocabularyAttribute>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub children: Option<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VocabularyAttribute {
pub id: String,
pub value: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct EPCISBody {
pub event_list: Vec<EPCISEvent>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::events::ObjectEvent;
use crate::types::Action;
use chrono::Utc;
#[test]
fn test_xml_roundtrip() {
let event = ObjectEvent::new(Utc::now(), "+00:00".to_string(), Action::Observe);
let doc = EPCISDocument::new(vec![EPCISEvent::ObjectEvent(event)]);
let xml_output = doc.to_xml();
assert!(xml_output.is_ok());
let xml_str = xml_output.unwrap();
assert!(xml_str.contains("<EPCISDocument"));
assert!(xml_str.contains("<epcisBody>"));
assert!(xml_str.contains("<type>ObjectEvent</type>"));
let deserialized = EPCISDocument::from_xml(&xml_str);
assert!(deserialized.is_ok());
let parsed_doc = deserialized.unwrap();
assert_eq!(parsed_doc.schema_version, "2.0");
assert_eq!(parsed_doc.r#type, "EPCISDocument");
}
}