use serde::{Deserialize, Deserializer, Serialize, Serializer};
use super::aluminium::AluminiumData;
use super::battery::BatteryData;
use super::construction::ConstructionData;
use super::detergent::DetergentData;
use super::electronics::ElectronicsData;
use super::furniture::FurnitureData;
use super::steel::SteelData;
use super::textile::TextileData;
use super::toy::ToyData;
use super::tyre::TyreData;
use super::unsold_goods::UnsoldGoodsReport;
use crate::domain::sector::Sector;
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub enum SectorData {
Battery(Box<BatteryData>),
Textile(Box<TextileData>),
UnsoldGoods(UnsoldGoodsReport),
Steel(SteelData),
Electronics(ElectronicsData),
Construction(ConstructionData),
Tyre(TyreData),
Toy(ToyData),
Aluminium(AluminiumData),
Furniture(FurnitureData),
Detergent(DetergentData),
Other {
sector: String,
data: serde_json::Value,
},
}
impl Serialize for SectorData {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
let mut value = match self {
Self::Battery(d) => serde_json::to_value(d),
Self::Textile(d) => serde_json::to_value(d),
Self::UnsoldGoods(d) => serde_json::to_value(d),
Self::Steel(d) => serde_json::to_value(d),
Self::Electronics(d) => serde_json::to_value(d),
Self::Construction(d) => serde_json::to_value(d),
Self::Tyre(d) => serde_json::to_value(d),
Self::Toy(d) => serde_json::to_value(d),
Self::Aluminium(d) => serde_json::to_value(d),
Self::Furniture(d) => serde_json::to_value(d),
Self::Detergent(d) => serde_json::to_value(d),
Self::Other { data, .. } => Ok(data.clone()),
}
.map_err(serde::ser::Error::custom)?;
let tag = self.sector();
if let serde_json::Value::Object(ref mut map) = value {
map.insert(
"sector".to_owned(),
serde_json::Value::String(tag.wire_str().to_owned()),
);
}
value.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for SectorData {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
use serde::de::Error as _;
let value = serde_json::Value::deserialize(deserializer)?;
let tag = value
.get("sector")
.and_then(serde_json::Value::as_str)
.ok_or_else(|| D::Error::missing_field("sector"))?
.to_owned();
macro_rules! typed {
($variant:ident) => {
serde_json::from_value(value.clone())
.map(Self::$variant)
.map_err(D::Error::custom)
};
}
match Sector::from_wire_tag(&tag) {
Sector::Battery => typed!(Battery),
Sector::Textile => typed!(Textile),
Sector::UnsoldGoods => typed!(UnsoldGoods),
Sector::Steel => typed!(Steel),
Sector::Electronics => typed!(Electronics),
Sector::Construction => typed!(Construction),
Sector::Tyre => typed!(Tyre),
Sector::Toy => typed!(Toy),
Sector::Aluminium => typed!(Aluminium),
Sector::Furniture => typed!(Furniture),
Sector::Detergent => typed!(Detergent),
Sector::Other(sector) => Ok(Self::Other {
sector,
data: value,
}),
}
}
}
impl SectorData {
#[must_use]
pub fn other(mut data: serde_json::Value) -> Option<Self> {
let map = data.as_object_mut()?;
let sector = map
.get("sector")
.and_then(serde_json::Value::as_str)
.unwrap_or("other")
.to_owned();
map.entry("sector")
.or_insert_with(|| serde_json::Value::String(sector.clone()));
match Sector::from_wire_tag(§or) {
Sector::Other(_) => Some(Self::Other { sector, data }),
_ => None,
}
}
pub fn sector(&self) -> Sector {
match self {
SectorData::Battery(_) => Sector::Battery,
SectorData::Textile(_) => Sector::Textile,
SectorData::UnsoldGoods(_) => Sector::UnsoldGoods,
SectorData::Steel(_) => Sector::Steel,
SectorData::Electronics(_) => Sector::Electronics,
SectorData::Construction(_) => Sector::Construction,
SectorData::Tyre(_) => Sector::Tyre,
SectorData::Toy(_) => Sector::Toy,
SectorData::Aluminium(_) => Sector::Aluminium,
SectorData::Furniture(_) => Sector::Furniture,
SectorData::Detergent(_) => Sector::Detergent,
SectorData::Other { sector, .. } => Sector::Other(sector.clone()),
}
}
#[must_use]
pub fn model_identifier(&self) -> Option<&str> {
match self {
SectorData::Battery(d) => d.battery_model_id.as_deref(),
SectorData::Textile(_)
| SectorData::UnsoldGoods(_)
| SectorData::Steel(_)
| SectorData::Electronics(_)
| SectorData::Construction(_)
| SectorData::Tyre(_)
| SectorData::Toy(_)
| SectorData::Aluminium(_)
| SectorData::Furniture(_)
| SectorData::Detergent(_)
| SectorData::Other { .. } => None,
}
}
pub fn gtin(&self) -> Option<&str> {
match self {
SectorData::Battery(d) => Some(d.gtin.as_str()),
SectorData::Textile(d) => Some(d.gtin.as_str()),
SectorData::Steel(d) => Some(d.gtin.as_str()),
SectorData::Electronics(d) => Some(d.gtin.as_str()),
SectorData::Construction(d) => Some(d.gtin.as_str()),
SectorData::Tyre(d) => Some(d.gtin.as_str()),
SectorData::Toy(d) => Some(d.gtin.as_str()),
SectorData::Aluminium(d) => Some(d.gtin.as_str()),
SectorData::Furniture(d) => Some(d.gtin.as_str()),
SectorData::Detergent(d) => Some(d.gtin.as_str()),
SectorData::UnsoldGoods(_) | SectorData::Other { .. } => None,
}
}
}
pub fn redact_sector_data(
data: &SectorData,
audience: crate::domain::identity::Audience,
descriptor: &crate::catalog::SectorDescriptor,
) -> serde_json::Value {
let mut value = match serde_json::to_value(data) {
Ok(v) => v,
Err(_) => return serde_json::Value::Null,
};
if let Some(obj) = value.as_object_mut() {
obj.retain(|key, _| match descriptor.disclosure.get(key) {
Some(&class) => audience.may_see(class),
None => true,
});
}
value
}
#[cfg(test)]
mod other_constructor_tests {
use super::*;
use serde_json::json;
#[test]
fn a_non_object_payload_is_refused() {
for payload in [
json!([{ "secret": "value" }]),
json!(42),
json!("battery"),
json!(null),
json!(true),
] {
assert!(
SectorData::other(payload.clone()).is_none(),
"a non-object payload must not become SectorData::Other: {payload}"
);
}
}
#[test]
fn an_object_for_an_unknown_sector_is_still_accepted() {
let tagged = SectorData::other(json!({ "sector": "quantum-widget", "spinPct": 3 }))
.expect("an unknown tagged sector is representable");
assert_eq!(tagged.sector(), Sector::Other("quantum-widget".into()));
let untagged = SectorData::other(json!({ "spinPct": 3 }))
.expect("an untagged object defaults to other");
assert_eq!(untagged.sector(), Sector::Other("other".into()));
}
#[test]
fn a_typed_sectors_tag_is_still_refused() {
assert!(SectorData::other(json!({ "sector": "battery" })).is_none());
}
#[test]
fn everything_accepted_round_trips() {
for payload in [
json!({ "sector": "quantum-widget", "spinPct": 3 }),
json!({ "spinPct": 3 }),
json!({}),
] {
let Some(built) = SectorData::other(payload.clone()) else {
continue;
};
let wire = serde_json::to_value(&built).expect("serialises");
assert!(
wire.get("sector")
.and_then(serde_json::Value::as_str)
.is_some(),
"the serialised form must carry a sector tag: {wire}"
);
let back: SectorData = serde_json::from_value(wire).expect("round-trips");
assert_eq!(back, built);
}
}
}