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
92impl Serialize for Event {
93    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
94    where
95        S: Serializer,
96    {
97        use serde::ser::SerializeSeq;
98
99        match self {
100            Event::Observe {
101                observation_id,
102                timestamp,
103                change_count,
104            } => {
105                let mut seq = serializer.serialize_seq(Some(4))?;
106                seq.serialize_element("observe")?;
107                seq.serialize_element(observation_id)?;
108                seq.serialize_element(timestamp)?;
109                seq.serialize_element(change_count)?;
110                seq.end()
111            }
112            Event::Add {
113                path,
114                value,
115                observation_id,
116            } => {
117                let mut seq = serializer.serialize_seq(Some(4))?;
118                seq.serialize_element("add")?;
119                seq.serialize_element(path)?;
120                seq.serialize_element(value)?;
121                seq.serialize_element(observation_id)?;
122                seq.end()
123            }
124            Event::Change {
125                path,
126                new_value,
127                observation_id,
128            } => {
129                let mut seq = serializer.serialize_seq(Some(4))?;
130                seq.serialize_element("change")?;
131                seq.serialize_element(path)?;
132                seq.serialize_element(new_value)?;
133                seq.serialize_element(observation_id)?;
134                seq.end()
135            }
136            Event::Remove {
137                path,
138                observation_id,
139            } => {
140                let mut seq = serializer.serialize_seq(Some(3))?;
141                seq.serialize_element("remove")?;
142                seq.serialize_element(path)?;
143                seq.serialize_element(observation_id)?;
144                seq.end()
145            }
146            Event::Move {
147                path,
148                moves,
149                observation_id,
150            } => {
151                let mut seq = serializer.serialize_seq(Some(4))?;
152                seq.serialize_element("move")?;
153                seq.serialize_element(path)?;
154                seq.serialize_element(moves)?;
155                seq.serialize_element(observation_id)?;
156                seq.end()
157            }
158            Event::Snapshot {
159                observation_id,
160                timestamp,
161                object,
162            } => {
163                let mut seq = serializer.serialize_seq(Some(4))?;
164                seq.serialize_element("snapshot")?;
165                seq.serialize_element(observation_id)?;
166                seq.serialize_element(timestamp)?;
167                seq.serialize_element(object)?;
168                seq.end()
169            }
170        }
171    }
172}
173
174struct EventVisitor;
175
176impl<'de> Visitor<'de> for EventVisitor {
177    type Value = Event;
178
179    fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
180        formatter.write_str("a JSON array representing an Event")
181    }
182
183    fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error>
184    where
185        A: SeqAccess<'de>,
186    {
187        let event_type: String = seq
188            .next_element()?
189            .ok_or_else(|| de::Error::missing_field("event type"))?;
190
191        match event_type.as_str() {
192            "observe" => {
193                let observation_id: String = seq
194                    .next_element()?
195                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
196                let timestamp: DateTime<Utc> = seq
197                    .next_element()?
198                    .ok_or_else(|| de::Error::missing_field("timestamp"))?;
199                let change_count: usize = seq
200                    .next_element()?
201                    .ok_or_else(|| de::Error::missing_field("change_count"))?;
202                Ok(Event::Observe {
203                    observation_id,
204                    timestamp,
205                    change_count,
206                })
207            }
208            "add" => {
209                let path: String = seq
210                    .next_element()?
211                    .ok_or_else(|| de::Error::missing_field("path"))?;
212                let value: Value = seq
213                    .next_element()?
214                    .ok_or_else(|| de::Error::missing_field("value"))?;
215                let observation_id: String = seq
216                    .next_element()?
217                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
218                Ok(Event::Add {
219                    path,
220                    value,
221                    observation_id,
222                })
223            }
224            "change" => {
225                let path: String = seq
226                    .next_element()?
227                    .ok_or_else(|| de::Error::missing_field("path"))?;
228                let new_value: Value = seq
229                    .next_element()?
230                    .ok_or_else(|| de::Error::missing_field("new_value"))?;
231                let observation_id: String = seq
232                    .next_element()?
233                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
234                Ok(Event::Change {
235                    path,
236                    new_value,
237                    observation_id,
238                })
239            }
240            "remove" => {
241                let path: String = seq
242                    .next_element()?
243                    .ok_or_else(|| de::Error::missing_field("path"))?;
244                let observation_id: String = seq
245                    .next_element()?
246                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
247                Ok(Event::Remove {
248                    path,
249                    observation_id,
250                })
251            }
252            "move" => {
253                let path: String = seq
254                    .next_element()?
255                    .ok_or_else(|| de::Error::missing_field("path"))?;
256                let moves: Vec<(usize, usize)> = seq
257                    .next_element()?
258                    .ok_or_else(|| de::Error::missing_field("moves"))?;
259                let observation_id: String = seq
260                    .next_element()?
261                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
262                Ok(Event::Move {
263                    path,
264                    moves,
265                    observation_id,
266                })
267            }
268            "snapshot" => {
269                let observation_id: String = seq
270                    .next_element()?
271                    .ok_or_else(|| de::Error::missing_field("observation_id"))?;
272                let timestamp: DateTime<Utc> = seq
273                    .next_element()?
274                    .ok_or_else(|| de::Error::missing_field("timestamp"))?;
275                let object: Value = seq
276                    .next_element()?
277                    .ok_or_else(|| de::Error::missing_field("object"))?;
278                Ok(Event::Snapshot {
279                    observation_id,
280                    timestamp,
281                    object,
282                })
283            }
284            _ => Err(de::Error::unknown_variant(
285                &event_type,
286                &["observe", "add", "change", "remove", "move", "snapshot"],
287            )),
288        }
289    }
290}
291
292impl<'de> Deserialize<'de> for Event {
293    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
294    where
295        D: Deserializer<'de>,
296    {
297        deserializer.deserialize_seq(EventVisitor)
298    }
299}
300
301#[derive(Debug, Clone)]
302pub struct Observation {
303    pub id: String,
304    pub timestamp: DateTime<Utc>,
305    pub events: Vec<Event>,
306}
307
308impl Observation {
309    pub fn new(id: String, timestamp: DateTime<Utc>) -> Self {
310        Self {
311            id,
312            timestamp,
313            events: Vec::new(),
314        }
315    }
316
317    pub fn add_event(&mut self, event: Event) {
318        self.events.push(event);
319    }
320
321    pub fn to_events(self) -> Vec<Event> {
322        let mut result = vec![Event::Observe {
323            observation_id: self.id.clone(),
324            timestamp: self.timestamp,
325            change_count: self.events.len(),
326        }];
327        result.extend(self.events);
328        result
329    }
330}
331
332#[cfg(test)]
333mod tests {
334    use super::*;
335    use serde_json::json;
336
337    #[test]
338    fn test_header_serialization() {
339        let header = Header::new(json!({"test": "value"}), Some("test-source".to_string()));
340        let serialized = serde_json::to_string(&header).unwrap();
341        let deserialized: Header = serde_json::from_str(&serialized).unwrap();
342
343        assert_eq!(header.file_type, deserialized.file_type);
344        assert_eq!(header.version, deserialized.version);
345        assert_eq!(header.initial, deserialized.initial);
346        assert_eq!(header.source, deserialized.source);
347    }
348
349    #[test]
350    fn test_event_serialization() {
351        let timestamp = Utc::now();
352
353        // Test observe event
354        let observe_event = Event::Observe {
355            observation_id: "obs-1".to_string(),
356            timestamp,
357            change_count: 2,
358        };
359        let serialized = serde_json::to_string(&observe_event).unwrap();
360        let expected_array = json!(["observe", "obs-1", timestamp, 2]);
361        assert_eq!(
362            serde_json::from_str::<Value>(&serialized).unwrap(),
363            expected_array
364        );
365
366        // Test add event
367        let add_event = Event::Add {
368            path: "/test".to_string(),
369            value: json!("value"),
370            observation_id: "obs-1".to_string(),
371        };
372        let serialized = serde_json::to_string(&add_event).unwrap();
373        let expected_array = json!(["add", "/test", "value", "obs-1"]);
374        assert_eq!(
375            serde_json::from_str::<Value>(&serialized).unwrap(),
376            expected_array
377        );
378
379        // Test all event types for serialization/deserialization round-trip
380        let events = vec![
381            Event::Observe {
382                observation_id: "obs-1".to_string(),
383                timestamp,
384                change_count: 2,
385            },
386            Event::Add {
387                path: "/test".to_string(),
388                value: json!("value"),
389                observation_id: "obs-1".to_string(),
390            },
391            Event::Change {
392                path: "/test".to_string(),
393                new_value: json!("new"),
394                observation_id: "obs-1".to_string(),
395            },
396            Event::Remove {
397                path: "/test".to_string(),
398                observation_id: "obs-1".to_string(),
399            },
400            Event::Move {
401                path: "/items".to_string(),
402                moves: vec![(0, 1)],
403                observation_id: "obs-1".to_string(),
404            },
405            Event::Snapshot {
406                observation_id: "snap-1".to_string(),
407                timestamp,
408                object: json!({"test": "state"}),
409            },
410        ];
411
412        for event in events {
413            let serialized = serde_json::to_string(&event).unwrap();
414
415            // Verify it's serialized as an array
416            let as_value: Value = serde_json::from_str(&serialized).unwrap();
417            assert!(as_value.is_array(), "Event should serialize to JSON array");
418
419            // Verify round-trip serialization/deserialization works
420            let deserialized: Event = serde_json::from_str(&serialized).unwrap();
421            let reserialized = serde_json::to_string(&deserialized).unwrap();
422            assert_eq!(
423                serialized, reserialized,
424                "Round-trip serialization should be identical"
425            );
426        }
427    }
428
429    #[test]
430    fn test_observation_to_events() {
431        let mut obs = Observation::new("obs-1".to_string(), Utc::now());
432        obs.add_event(Event::Add {
433            path: "/test".to_string(),
434            value: json!("value"),
435            observation_id: "obs-1".to_string(),
436        });
437        obs.add_event(Event::Change {
438            path: "/test".to_string(),
439            new_value: json!("new"),
440            observation_id: "obs-1".to_string(),
441        });
442
443        let events = obs.to_events();
444        assert_eq!(events.len(), 3); // observe + 2 events
445
446        match &events[0] {
447            Event::Observe { change_count, .. } => assert_eq!(*change_count, 2),
448            _ => panic!("First event should be observe"),
449        }
450    }
451}