1use chrono::{DateTime, Utc};
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4use sha2::{Digest, Sha256};
5use uuid::Uuid;
6
7#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
29pub struct SyncEvent {
30 pub id: Uuid,
32 pub sequence: u64,
34 pub event_type: String,
36 pub entity_type: String,
38 pub entity_id: String,
40 pub payload: Value,
42 pub hash: String,
44 pub signature: Option<String>,
46 pub timestamp: DateTime<Utc>,
48}
49
50impl SyncEvent {
51 #[must_use]
53 pub fn new(
54 event_type: impl Into<String>,
55 entity_type: impl Into<String>,
56 entity_id: impl Into<String>,
57 payload: Value,
58 ) -> Self {
59 let hash = Self::compute_hash(&payload);
60 Self {
61 id: Uuid::new_v4(),
62 sequence: 0,
63 event_type: event_type.into(),
64 entity_type: entity_type.into(),
65 entity_id: entity_id.into(),
66 payload,
67 hash,
68 signature: None,
69 timestamp: Utc::now(),
70 }
71 }
72
73 #[must_use]
75 pub fn with_id(
76 id: Uuid,
77 sequence: u64,
78 event_type: impl Into<String>,
79 entity_type: impl Into<String>,
80 entity_id: impl Into<String>,
81 payload: Value,
82 timestamp: DateTime<Utc>,
83 ) -> Self {
84 let hash = Self::compute_hash(&payload);
85 Self {
86 id,
87 sequence,
88 event_type: event_type.into(),
89 entity_type: entity_type.into(),
90 entity_id: entity_id.into(),
91 payload,
92 hash,
93 signature: None,
94 timestamp,
95 }
96 }
97
98 #[must_use]
100 pub fn compute_hash(payload: &Value) -> String {
101 let canonical = canonicalize_json(payload);
102 let bytes = serde_json::to_vec(&canonical).unwrap_or_default();
103 let mut hasher = Sha256::new();
104 hasher.update(&bytes);
105 hex::encode(hasher.finalize())
106 }
107
108 #[must_use]
110 pub const fn with_sequence(mut self, sequence: u64) -> Self {
111 self.sequence = sequence;
112 self
113 }
114
115 #[must_use]
117 pub fn with_signature(mut self, signature: impl Into<String>) -> Self {
118 self.signature = Some(signature.into());
119 self
120 }
121}
122
123impl PartialOrd for SyncEvent {
124 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
125 Some(self.cmp(other))
126 }
127}
128
129impl Ord for SyncEvent {
130 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
131 self.sequence
132 .cmp(&other.sequence)
133 .then_with(|| self.timestamp.cmp(&other.timestamp))
134 .then_with(|| self.id.cmp(&other.id))
135 }
136}
137
138fn canonicalize_json(value: &Value) -> Value {
139 match value {
140 Value::Object(map) => {
141 let mut keys: Vec<&String> = map.keys().collect();
142 keys.sort_unstable();
143
144 let mut canonical = Map::with_capacity(map.len());
145 for key in keys {
146 if let Some(inner) = map.get(key) {
147 canonical.insert(key.clone(), canonicalize_json(inner));
148 }
149 }
150 Value::Object(canonical)
151 }
152 Value::Array(values) => Value::Array(values.iter().map(canonicalize_json).collect()),
153 _ => value.clone(),
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use serde_json::json;
161
162 #[test]
163 fn new_event_has_uuid_and_hash() {
164 let event = SyncEvent::new("order.created", "order", "ORD-1", json!({"total": 10}));
165 assert!(!event.id.is_nil());
166 assert!(!event.hash.is_empty());
167 assert_eq!(event.hash.len(), 64); assert_eq!(event.sequence, 0);
169 assert!(event.signature.is_none());
170 }
171
172 #[test]
173 fn event_serde_roundtrip() {
174 let event =
175 SyncEvent::new("product.updated", "product", "PROD-1", json!({"name": "Widget"}));
176 let json = serde_json::to_string(&event).unwrap();
177 let deserialized: SyncEvent = serde_json::from_str(&json).unwrap();
178 assert_eq!(deserialized.id, event.id);
179 assert_eq!(deserialized.event_type, event.event_type);
180 assert_eq!(deserialized.hash, event.hash);
181 assert_eq!(deserialized.payload, event.payload);
182 }
183
184 #[test]
185 fn event_with_sequence() {
186 let event = SyncEvent::new("order.created", "order", "ORD-1", json!({})).with_sequence(42);
187 assert_eq!(event.sequence, 42);
188 }
189
190 #[test]
191 fn event_with_signature() {
192 let event =
193 SyncEvent::new("order.created", "order", "ORD-1", json!({})).with_signature("deadbeef");
194 assert_eq!(event.signature, Some("deadbeef".to_string()));
195 }
196
197 #[test]
198 fn event_ordering_by_sequence() {
199 let e1 = SyncEvent::new("a", "x", "1", json!({})).with_sequence(1);
200 let e2 = SyncEvent::new("b", "x", "2", json!({})).with_sequence(2);
201 let e3 = SyncEvent::new("c", "x", "3", json!({})).with_sequence(3);
202
203 let mut events = vec![e3, e1, e2];
204 events.sort();
205 assert_eq!(events[0].sequence, 1);
206 assert_eq!(events[1].sequence, 2);
207 assert_eq!(events[2].sequence, 3);
208 }
209
210 #[test]
211 fn same_payload_same_hash() {
212 let payload = json!({"key": "value"});
213 let e1 = SyncEvent::new("a", "x", "1", payload.clone());
214 let e2 = SyncEvent::new("b", "y", "2", payload);
215 assert_eq!(e1.hash, e2.hash);
216 }
217
218 #[test]
219 fn different_payload_different_hash() {
220 let e1 = SyncEvent::new("a", "x", "1", json!({"key": "value1"}));
221 let e2 = SyncEvent::new("a", "x", "1", json!({"key": "value2"}));
222 assert_ne!(e1.hash, e2.hash);
223 }
224
225 #[test]
226 fn with_id_constructor() {
227 let id = Uuid::new_v4();
228 let ts = Utc::now();
229 let event = SyncEvent::with_id(id, 10, "order.created", "order", "ORD-1", json!({}), ts);
230 assert_eq!(event.id, id);
231 assert_eq!(event.sequence, 10);
232 assert_eq!(event.timestamp, ts);
233 }
234
235 #[test]
236 fn event_eq() {
237 let id = Uuid::new_v4();
238 let ts = Utc::now();
239 let e1 = SyncEvent::with_id(id, 1, "a", "b", "c", json!({}), ts);
240 let e2 = SyncEvent::with_id(id, 1, "a", "b", "c", json!({}), ts);
241 assert_eq!(e1, e2);
242 }
243
244 #[test]
245 fn event_debug() {
246 let event = SyncEvent::new("test", "entity", "id", json!({}));
247 let debug = format!("{event:?}");
248 assert!(debug.contains("SyncEvent"));
249 }
250
251 #[test]
252 fn compute_hash_deterministic() {
253 let payload = json!({"a": 1, "b": 2});
254 let h1 = SyncEvent::compute_hash(&payload);
255 let h2 = SyncEvent::compute_hash(&payload);
256 assert_eq!(h1, h2);
257 }
258
259 #[test]
260 fn compute_hash_is_canonical_for_object_key_order() {
261 let p1 = json!({"a": 1, "b": 2, "c": {"x": 1, "y": 2}});
262 let p2 = json!({"c": {"y": 2, "x": 1}, "b": 2, "a": 1});
263 assert_eq!(SyncEvent::compute_hash(&p1), SyncEvent::compute_hash(&p2));
264 }
265}