use std::collections::BTreeMap;
use ipld_core::ipld::Ipld;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Tombstone {
pub reason: String,
pub tombstoned_at: u64,
pub extra: BTreeMap<String, Ipld>,
}
impl Tombstone {
pub const KIND: &'static str = "tombstone";
#[must_use]
pub fn new(reason: impl Into<String>, tombstoned_at: u64) -> Self {
Self {
reason: reason.into(),
tombstoned_at,
extra: BTreeMap::new(),
}
}
}
#[derive(Serialize, Deserialize)]
struct TombstoneWire {
#[serde(rename = "_kind")]
kind: String,
reason: String,
tombstoned_at: u64,
#[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
extra: BTreeMap<String, Ipld>,
}
impl Serialize for Tombstone {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
TombstoneWire {
kind: Self::KIND.into(),
reason: self.reason.clone(),
tombstoned_at: self.tombstoned_at,
extra: self.extra.clone(),
}
.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Tombstone {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let w = TombstoneWire::deserialize(deserializer)?;
if w.kind != Self::KIND {
return Err(serde::de::Error::custom(format!(
"expected _kind='{}', got '{}'",
Self::KIND,
w.kind
)));
}
Ok(Self {
reason: w.reason,
tombstoned_at: w.tombstoned_at,
extra: w.extra,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::codec::{from_canonical_bytes, to_canonical_bytes};
#[test]
fn tombstone_round_trip_byte_identity() {
let t = Tombstone::new("user asked to forget", 1_700_000_000_000_000);
let bytes = to_canonical_bytes(&t).unwrap();
let decoded: Tombstone = from_canonical_bytes(&bytes).unwrap();
assert_eq!(t, decoded);
let bytes2 = to_canonical_bytes(&decoded).unwrap();
assert_eq!(bytes, bytes2);
}
#[test]
fn tombstone_kind_rejection() {
let w = TombstoneWire {
kind: "node".into(),
reason: "x".into(),
tombstoned_at: 0,
extra: BTreeMap::new(),
};
let bytes = serde_ipld_dagcbor::to_vec(&w).unwrap();
let err = serde_ipld_dagcbor::from_slice::<Tombstone>(&bytes).unwrap_err();
assert!(err.to_string().contains("_kind"));
}
#[test]
fn tombstone_empty_reason_round_trips() {
let t = Tombstone::new("", 42);
let bytes = to_canonical_bytes(&t).unwrap();
let decoded: Tombstone = from_canonical_bytes(&bytes).unwrap();
assert_eq!(t, decoded);
assert!(decoded.reason.is_empty());
}
}