#![deny(missing_docs)]
#![deny(unsafe_code)]
#![warn(clippy::pedantic)]
#![allow(clippy::module_name_repetitions)]
use serde::{Deserialize, Serialize};
use chrono::{DateTime, Utc};
use std::borrow::Cow;
use crate::error::EpcisModelError;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Epc(pub Cow<'static, str>);
impl From<String> for Epc {
fn from(s: String) -> Self {
Epc(Cow::Owned(s))
}
}
impl From<Cow<'static, str>> for Epc {
fn from(s: Cow<'static, str>) -> Self {
Epc(s)
}
}
impl TryFrom<&str> for Epc {
type Error = EpcisModelError;
fn try_from(s: &str) -> Result<Self, Self::Error> {
if s.is_empty() {
return Err(EpcisModelError::InvalidEpc("EPC URN cannot be empty".to_string()));
}
if !s.starts_with("urn:epc:") && !s.starts_with("http://") && !s.starts_with("https://") {
return Err(EpcisModelError::InvalidEpc(format!("EPC URN must start with URN or HTTP scheme: {s}")));
}
Ok(Epc(Cow::Owned(s.to_string())))
}
}
impl std::fmt::Display for Epc {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum Action {
Add,
Observe,
Delete,
}
impl<'de> Deserialize<'de> for Action {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let s = crate::document::deserialize_string_or_map_text(deserializer)?;
match s.as_str() {
"ADD" => Ok(Action::Add),
"OBSERVE" => Ok(Action::Observe),
"DELETE" => Ok(Action::Delete),
other => Err(serde::de::Error::custom(format!(
"invalid action: {}, expected ADD, OBSERVE, or DELETE",
other
))),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ReadPointId(pub Cow<'static, str>);
impl From<&'static str> for ReadPointId {
fn from(s: &'static str) -> Self {
ReadPointId(Cow::Borrowed(s))
}
}
impl From<String> for ReadPointId {
fn from(s: String) -> Self {
ReadPointId(Cow::Owned(s))
}
}
impl std::fmt::Display for ReadPointId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ReadPoint {
pub id: ReadPointId,
}
impl From<&'static str> for ReadPoint {
fn from(id: &'static str) -> Self {
ReadPoint { id: ReadPointId::from(id) }
}
}
impl From<String> for ReadPoint {
fn from(id: String) -> Self {
ReadPoint { id: ReadPointId::from(id) }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BizLocationId(pub Cow<'static, str>);
impl From<&'static str> for BizLocationId {
fn from(s: &'static str) -> Self {
BizLocationId(Cow::Borrowed(s))
}
}
impl From<String> for BizLocationId {
fn from(s: String) -> Self {
BizLocationId(Cow::Owned(s))
}
}
impl std::fmt::Display for BizLocationId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct BizLocation {
pub id: BizLocationId,
}
impl From<&'static str> for BizLocation {
fn from(id: &'static str) -> Self {
BizLocation { id: BizLocationId::from(id) }
}
}
impl From<String> for BizLocation {
fn from(id: String) -> Self {
BizLocation { id: BizLocationId::from(id) }
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BizTransaction {
#[serde(rename = "type")]
pub r#type: String,
pub biz_transaction: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
#[serde(rename = "type")]
pub r#type: String,
pub source: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Destination {
#[serde(rename = "type")]
pub r#type: String,
pub destination: String,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct QuantityElement {
pub epc_class: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub quantity: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uom: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SensorMetadata {
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<DateTime<Utc>>,
#[serde(rename = "deviceID", skip_serializing_if = "Option::is_none")]
pub device_id: Option<String>,
#[serde(rename = "deviceMetadataURI", skip_serializing_if = "Option::is_none")]
pub device_metadata_uri: Option<String>,
#[serde(rename = "rawDataURI", skip_serializing_if = "Option::is_none")]
pub raw_data_uri: Option<String>,
#[serde(rename = "dataContentURI", skip_serializing_if = "Option::is_none")]
pub data_content_uri: Option<String>,
#[serde(rename = "bizRulesURI", skip_serializing_if = "Option::is_none")]
pub biz_rules_uri: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SensorReport {
#[serde(rename = "type")]
pub r#type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub value: Option<f64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uom: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sensor_processor: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub time: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub microsecond_offset: Option<i32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub chemical_substance: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub data_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub string_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub boolean_value: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub hex_binary_value: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub uri_value: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SensorElement {
#[serde(skip_serializing_if = "Option::is_none")]
pub sensor_metadata: Option<SensorMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub sensor_report: Option<Vec<SensorReport>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ErrorDeclaration {
pub declaration_time: DateTime<Utc>,
#[serde(skip_serializing_if = "Option::is_none")]
pub reason: Option<String>,
#[serde(rename = "correctiveEventIDs", skip_serializing_if = "Option::is_none")]
pub corrective_event_ids: Option<Vec<String>>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::cbv::BizStep;
use std::str::FromStr;
#[test]
fn test_epc_validation() {
let epc = Epc::try_from("urn:epc:id:sgtin:0614141.107346.2023");
assert!(epc.is_ok());
assert_eq!(epc.unwrap().to_string(), "urn:epc:id:sgtin:0614141.107346.2023");
let epc_uri = Epc::try_from("https://id.gs1.org/01/04012345987652/21/12345");
assert!(epc_uri.is_ok());
let empty = Epc::try_from("");
assert!(empty.is_err());
assert!(matches!(empty.unwrap_err(), EpcisModelError::InvalidEpc(_)));
let bad_epc = Epc::try_from("bad:scheme:123");
assert!(bad_epc.is_err());
}
#[test]
fn test_action_deserialization_errors() {
let observe: Action = serde_json::from_str("\"OBSERVE\"").unwrap();
assert_eq!(observe, Action::Observe);
let err: Result<Action, _> = serde_json::from_str("\"INVALID\"");
assert!(err.is_err());
}
#[test]
fn test_biz_step_custom_and_standard() {
let step = BizStep::from_str("urn:epcglobal:cbv:bizstep:receiving").unwrap();
assert_eq!(step.as_str(), "urn:epcglobal:cbv:bizstep:receiving");
let custom = BizStep::from_str("https://example.com/custom-bizstep").unwrap();
assert_eq!(custom.as_str(), "https://example.com/custom-bizstep");
}
}