mnem_core/objects/
tombstone.rs1use std::collections::BTreeMap;
26
27use ipld_core::ipld::Ipld;
28use serde::{Deserialize, Deserializer, Serialize, Serializer};
29
30#[derive(Clone, Debug, PartialEq, Eq)]
34pub struct Tombstone {
35 pub reason: String,
37 pub tombstoned_at: u64,
39 pub extra: BTreeMap<String, Ipld>,
41}
42
43impl Tombstone {
44 pub const KIND: &'static str = "tombstone";
46
47 #[must_use]
49 pub fn new(reason: impl Into<String>, tombstoned_at: u64) -> Self {
50 Self {
51 reason: reason.into(),
52 tombstoned_at,
53 extra: BTreeMap::new(),
54 }
55 }
56}
57
58#[derive(Serialize, Deserialize)]
61struct TombstoneWire {
62 #[serde(rename = "_kind")]
63 kind: String,
64 reason: String,
65 tombstoned_at: u64,
66 #[serde(flatten, default, skip_serializing_if = "BTreeMap::is_empty")]
67 extra: BTreeMap<String, Ipld>,
68}
69
70impl Serialize for Tombstone {
71 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
72 TombstoneWire {
73 kind: Self::KIND.into(),
74 reason: self.reason.clone(),
75 tombstoned_at: self.tombstoned_at,
76 extra: self.extra.clone(),
77 }
78 .serialize(serializer)
79 }
80}
81
82impl<'de> Deserialize<'de> for Tombstone {
83 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
84 let w = TombstoneWire::deserialize(deserializer)?;
85 if w.kind != Self::KIND {
86 return Err(serde::de::Error::custom(format!(
87 "expected _kind='{}', got '{}'",
88 Self::KIND,
89 w.kind
90 )));
91 }
92 Ok(Self {
93 reason: w.reason,
94 tombstoned_at: w.tombstoned_at,
95 extra: w.extra,
96 })
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103 use crate::codec::{from_canonical_bytes, to_canonical_bytes};
104
105 #[test]
106 fn tombstone_round_trip_byte_identity() {
107 let t = Tombstone::new("user asked to forget", 1_700_000_000_000_000);
108 let bytes = to_canonical_bytes(&t).unwrap();
109 let decoded: Tombstone = from_canonical_bytes(&bytes).unwrap();
110 assert_eq!(t, decoded);
111 let bytes2 = to_canonical_bytes(&decoded).unwrap();
112 assert_eq!(bytes, bytes2);
113 }
114
115 #[test]
116 fn tombstone_kind_rejection() {
117 let w = TombstoneWire {
118 kind: "node".into(),
119 reason: "x".into(),
120 tombstoned_at: 0,
121 extra: BTreeMap::new(),
122 };
123 let bytes = serde_ipld_dagcbor::to_vec(&w).unwrap();
124 let err = serde_ipld_dagcbor::from_slice::<Tombstone>(&bytes).unwrap_err();
125 assert!(err.to_string().contains("_kind"));
126 }
127
128 #[test]
129 fn tombstone_empty_reason_round_trips() {
130 let t = Tombstone::new("", 42);
131 let bytes = to_canonical_bytes(&t).unwrap();
132 let decoded: Tombstone = from_canonical_bytes(&bytes).unwrap();
133 assert_eq!(t, decoded);
134 assert!(decoded.reason.is_empty());
135 }
136}