json_archive/
events.rs

1// json-archive is a tool for tracking JSON file changes over time
2// Copyright (C) 2025  Peoples Grocers LLC
3//
4// This program is free software: you can redistribute it and/or modify
5// it under the terms of the GNU Affero General Public License as published
6// by the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// This program is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12// GNU Affero General Public License for more details.
13//
14// You should have received a copy of the GNU Affero General Public License
15// along with this program.  If not, see <https://www.gnu.org/licenses/>.
16//
17// To purchase a license under different terms contact admin@peoplesgrocers.com
18// To request changes, report bugs, or give user feedback contact
19// marxism@peoplesgrocers.com
20//
21
22use chrono::{DateTime, Utc};
23use serde::de::{self, SeqAccess, Visitor};
24use serde::{Deserialize, Deserializer, Serialize, Serializer};
25use serde_json::Value;
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Header {
29    /// Type identifier for magic file detection. We put this first to act as a "poor man's
30    /// magic file number detection" when the archive file has an unexpected extension.
31    /// This helps avoid issues like the Elm compiler that requires specific file extensions
32    /// (e.g., .js) which doesn't play nice with build systems using temporary files with
33    /// arbitrary suffixes. By putting this key first, we can detect archive files even
34    /// when they have non-standard extensions like .tmp appended by build tools.
35    #[serde(rename = "type")]
36    pub file_type: String,
37    pub version: u32,
38    pub created: DateTime<Utc>,
39    #[serde(skip_serializing_if = "Option::is_none")]
40    pub source: Option<String>,
41    pub initial: Value,
42    #[serde(skip_serializing_if = "Option::is_none")]
43    pub metadata: Option<Value>,
44}
45
46impl Header {
47    pub fn new(initial: Value, source: Option<String>) -> Self {
48        Self {
49            file_type: "@peoplesgrocers/json-archive".to_string(),
50            version: 1,
51            created: Utc::now(),
52            source,
53            initial,
54            metadata: None,
55        }
56    }
57}
58
59#[derive(Debug, Clone)]
60pub enum Event {
61    Observe {
62        observation_id: String,
63        timestamp: DateTime<Utc>,
64        change_count: usize,
65    },
66    Add {
67        path: String,
68        value: Value,
69        observation_id: String,
70    },
71    Change {
72        path: String,
73        new_value: Value,
74        observation_id: String,
75    },
76    Remove {
77        path: String,
78        observation_id: String,
79    },
80    Move {
81        path: String,
82        moves: Vec<(usize, usize)>,
83        observation_id: String,
84    },
85    Snapshot {
86        observation_id: String,
87        timestamp: DateTime<Utc>,
88        object: Value,
89    },
90}
91
92
93impl Serialize for Event {
94    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
95    where
96        S: Serializer,
97    {
98        use serde::ser::SerializeSeq;
99
100        match self {
101            Event::Observe {
102                observation_id,
103                timestamp,
104                change_count,
105            } => {
106                let mut seq = serializer.serialize_seq(Some(4))?;
107                seq.serialize_element("observe")?;
108                seq.serialize_element(observation_id)?;
109                seq.serialize_element(timestamp)?;
110                seq.serialize_element(change_count)?;
111                seq.end()
112            }
113            Event::Add {
114                path,
115                value,
116                observation_id,
117            } => {
118                let mut seq = serializer.serialize_seq(Some(4))?;
119                seq.serialize_element("add")?;
120                seq.serialize_element(path)?;
121                seq.serialize_element(value)?;
122                seq.serialize_element(observation_id)?;
123                seq.end()
124            }
125            Event::Change {
126                path,
127                new_value,
128                observation_id,
129            } => {
130                let mut seq = serializer.serialize_seq(Some(4))?;
131                seq.serialize_element("change")?;
132                seq.serialize_element(path)?;
133                seq.serialize_element(new_value)?;
134                seq.serialize_element(observation_id)?;
135                seq.end()
136            }
137            Event::Remove {
138                path,
139                observation_id,
140            } => {
141                let mut seq = serializer.serialize_seq(Some(3))?;
142                seq.serialize_element("remove")?;
143                seq.serialize_element(path)?;
144                seq.serialize_element(observation_id)?;
145                seq.end()
146            }
147            Event::Move {
148                path,
149                moves,
150                observation_id,
151            } => {
152                let mut seq = serializer.serialize_seq(Some(4))?;
153                seq.serialize_element("move")?;
154                seq.serialize_element(path)?;
155                seq.serialize_element(moves)?;
156                seq.serialize_element(observation_id)?;
157                seq.end()
158            }
159            Event::Snapshot {
160                observation_id,
161                timestamp,
162                object,
163            } => {
164                let mut seq = serializer.serialize_seq(Some(4))?;
165                seq.serialize_element("snapshot")?;
166                seq.serialize_element(observation_id)?;
167                seq.serialize_element(timestamp)?;
168                seq.serialize_element(object)?;
169                seq.end()
170            }
171        }
172    }
173}
174
175struct EventVisitor;
176
177impl<'de> Visitor<'de> for EventVisitor {
178    type Value = Event;
179
180    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
181        formatter.write_str("a JSON array representing an Event")
182    }
183
184    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
185    where
186        A: SeqAccess<'de>,
187    {
188        let event_type: String = seq
189            .next_element()?
190            .ok_or_else(|| de::Error::missing_field("event type"))?;
191
192        match event_type.as_str() {
193            "observe" => {
194                let observation_id: String = seq
195                    .next_element()?
196                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
197                let timestamp: DateTime<Utc> = seq
198                    .next_element()?
199                    .ok_or_else(|| de::Error::missing_field("timestamp"))?;
200                let change_count: usize = seq
201                    .next_element()?
202                    .ok_or_else(|| de::Error::missing_field("change_count"))?;
203                Ok(Event::Observe {
204                    observation_id,
205                    timestamp,
206                    change_count,
207                })
208            }
209            "add" => {
210                let path: String = seq
211                    .next_element()?
212                    .ok_or_else(|| de::Error::missing_field("path"))?;
213                let value: Value = seq
214                    .next_element()?
215                    .ok_or_else(|| de::Error::missing_field("value"))?;
216                let observation_id: String = seq
217                    .next_element()?
218                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
219                Ok(Event::Add {
220                    path,
221                    value,
222                    observation_id,
223                })
224            }
225            "change" => {
226                let path: String = seq
227                    .next_element()?
228                    .ok_or_else(|| de::Error::missing_field("path"))?;
229                let new_value: Value = seq
230                    .next_element()?
231                    .ok_or_else(|| de::Error::missing_field("new_value"))?;
232                let observation_id: String = seq
233                    .next_element()?
234                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
235                Ok(Event::Change {
236                    path,
237                    new_value,
238                    observation_id,
239                })
240            }
241            "remove" => {
242                let path: String = seq
243                    .next_element()?
244                    .ok_or_else(|| de::Error::missing_field("path"))?;
245                let observation_id: String = seq
246                    .next_element()?
247                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
248                Ok(Event::Remove {
249                    path,
250                    observation_id,
251                })
252            }
253            "move" => {
254                let path: String = seq
255                    .next_element()?
256                    .ok_or_else(|| de::Error::missing_field("path"))?;
257                let moves: Vec<(usize, usize)> = seq
258                    .next_element()?
259                    .ok_or_else(|| de::Error::missing_field("moves"))?;
260                let observation_id: String = seq
261                    .next_element()?
262                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
263                Ok(Event::Move {
264                    path,
265                    moves,
266                    observation_id,
267                })
268            }
269            "snapshot" => {
270                let observation_id: String = seq
271                    .next_element()?
272                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
273                let timestamp: DateTime<Utc> = seq
274                    .next_element()?
275                    .ok_or_else(|| de::Error::missing_field("timestamp"))?;
276                let object: Value = seq
277                    .next_element()?
278                    .ok_or_else(|| de::Error::missing_field("object"))?;
279                Ok(Event::Snapshot {
280                    observation_id,
281                    timestamp,
282                    object,
283                })
284            }
285            _ => Err(de::Error::unknown_variant(
286                &event_type,
287                &["observe", "add", "change", "remove", "move", "snapshot"],
288            )),
289        }
290    }
291}
292
293impl<'de> Deserialize<'de> for Event {
294    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
295    where
296        D: Deserializer<'de>,
297    {
298        deserializer.deserialize_seq(EventVisitor)
299    }
300}
301
302#[derive(Debug, Clone)]
303pub struct Observation {
304    pub id: String,
305    pub timestamp: DateTime<Utc>,
306    pub events: Vec<Event>,
307}
308
309impl Observation {
310    pub fn new(id: String, timestamp: DateTime<Utc>) -> Self {
311        Self {
312            id,
313            timestamp,
314            events: Vec::new(),
315        }
316    }
317
318    pub fn add_event(&mut self, event: Event) {
319        self.events.push(event);
320    }
321
322    pub fn to_events(self) -> Vec<Event> {
323        let mut result = vec![Event::Observe {
324            observation_id: self.id.clone(),
325            timestamp: self.timestamp,
326            change_count: self.events.len(),
327        }];
328        result.extend(self.events);
329        result
330    }
331}
332
333#[cfg(test)]
334mod tests {
335    use super::*;
336    use serde_json::json;
337
338    #[test]
339    fn test_header_serialization() {
340        let header = Header::new(json!({"test": "value"}), Some("test-source".to_string()));
341        let serialized = serde_json::to_string(&header).unwrap();
342        let deserialized: Header = serde_json::from_str(&serialized).unwrap();
343
344        assert_eq!(header.file_type, deserialized.file_type);
345        assert_eq!(header.version, deserialized.version);
346        assert_eq!(header.initial, deserialized.initial);
347        assert_eq!(header.source, deserialized.source);
348    }
349
350    #[test]
351    fn test_event_serialization() {
352        let timestamp = Utc::now();
353
354        // Test observe event
355        let observe_event = Event::Observe {
356            observation_id: "obs-1".to_string(),
357            timestamp,
358            change_count: 2,
359        };
360        let serialized = serde_json::to_string(&observe_event).unwrap();
361        let expected_array = json!(["observe", "obs-1", timestamp, 2]);
362        assert_eq!(
363            serde_json::from_str::<Value>(&serialized).unwrap(),
364            expected_array
365        );
366
367        // Test add event
368        let add_event = Event::Add {
369            path: "/test".to_string(),
370            value: json!("value"),
371            observation_id: "obs-1".to_string(),
372        };
373        let serialized = serde_json::to_string(&add_event).unwrap();
374        let expected_array = json!(["add", "/test", "value", "obs-1"]);
375        assert_eq!(
376            serde_json::from_str::<Value>(&serialized).unwrap(),
377            expected_array
378        );
379
380        // Test all event types for serialization/deserialization round-trip
381        let events = vec![
382            Event::Observe {
383                observation_id: "obs-1".to_string(),
384                timestamp,
385                change_count: 2,
386            },
387            Event::Add {
388                path: "/test".to_string(),
389                value: json!("value"),
390                observation_id: "obs-1".to_string(),
391            },
392            Event::Change {
393                path: "/test".to_string(),
394                new_value: json!("new"),
395                observation_id: "obs-1".to_string(),
396            },
397            Event::Remove {
398                path: "/test".to_string(),
399                observation_id: "obs-1".to_string(),
400            },
401            Event::Move {
402                path: "/items".to_string(),
403                moves: vec![(0, 1)],
404                observation_id: "obs-1".to_string(),
405            },
406            Event::Snapshot {
407                observation_id: "snap-1".to_string(),
408                timestamp,
409                object: json!({"test": "state"}),
410            },
411        ];
412
413        for event in events {
414            let serialized = serde_json::to_string(&event).unwrap();
415
416            // Verify it's serialized as an array
417            let as_value: Value = serde_json::from_str(&serialized).unwrap();
418            assert!(as_value.is_array(), "Event should serialize to JSON array");
419
420            // Verify round-trip serialization/deserialization works
421            let deserialized: Event = serde_json::from_str(&serialized).unwrap();
422            let reserialized = serde_json::to_string(&deserialized).unwrap();
423            assert_eq!(
424                serialized, reserialized,
425                "Round-trip serialization should be identical"
426            );
427        }
428    }
429
430    #[test]
431    fn test_observation_to_events() {
432        let mut obs = Observation::new("obs-1".to_string(), Utc::now());
433        obs.add_event(Event::Add {
434            path: "/test".to_string(),
435            value: json!("value"),
436            observation_id: "obs-1".to_string(),
437        });
438        obs.add_event(Event::Change {
439            path: "/test".to_string(),
440            new_value: json!("new"),
441            observation_id: "obs-1".to_string(),
442        });
443
444        let events = obs.to_events();
445        assert_eq!(events.len(), 3); // observe + 2 events
446
447        match &events[0] {
448            Event::Observe { change_count, .. } => assert_eq!(*change_count, 2),
449            _ => panic!("First event should be observe"),
450        }
451    }
452}