Skip to main content

journalmap/
journalmap.rs

1use crate::JournalError;
2use crate::Tag;
3use std::collections::HashMap;
4use std::collections::HashSet;
5use std::fmt::Debug;
6use std::hash::Hash;
7use std::ops::Range;
8
9#[cfg(feature = "serde")]
10use serde::Deserialize;
11#[cfg(feature = "serde")]
12use serde::Serialize;
13#[cfg(feature = "serde")]
14use serde::de::Deserialize as DeserializeTrait;
15#[cfg(feature = "serde")]
16use serde::de::Deserializer;
17#[cfg(feature = "serde")]
18use serde::ser::Serialize as SerializeTrait;
19#[cfg(feature = "serde")]
20use serde::ser::Serializer;
21
22#[derive(Clone, Copy)]
23#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
24struct Insert<K, V> {
25    pub key: K,
26    pub val: V,
27    pub replacing: Option<V>,
28}
29
30impl<K: Eq + Hash + Clone, V: Clone> Insert<K, V> {
31    pub fn forward(&self, map: &mut HashMap<K, V>) {
32        map.insert(self.key.clone(), self.val.clone());
33    }
34
35    pub fn reverse(&self, map: &mut HashMap<K, V>) {
36        match self.replacing.clone() {
37            Some(val) => {
38                map.insert(self.key.clone(), val);
39            }
40            None => {
41                map.remove(&self.key);
42            }
43        }
44    }
45}
46
47impl<K: Debug, V: Debug> std::fmt::Debug for Insert<K, V> {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        match &self.replacing {
50            Some(replacing) => write!(f, "[>] {:?}:{:?}->{:?}", self.key, replacing, self.val),
51            None => write!(f, "[+] {:?}:{:?}", self.key, self.val),
52        }
53    }
54}
55
56#[derive(Clone, Copy)]
57#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
58struct Remove<K, V> {
59    pub key: K,
60    pub val: V,
61}
62
63impl<K: Eq + Hash + Clone, V: Clone> Remove<K, V> {
64    pub fn forward(&self, map: &mut HashMap<K, V>) {
65        map.remove(&self.key);
66    }
67
68    pub fn reverse(&self, map: &mut HashMap<K, V>) {
69        map.insert(self.key.clone(), self.val.clone());
70    }
71}
72
73impl<K: Debug, V: Debug> std::fmt::Debug for Remove<K, V> {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        write!(f, "[-] '{:?}':'{:?}'", self.key, self.val)
76    }
77}
78
79#[derive(Clone, Copy)]
80#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
81enum JournalEntry<K, V, T> {
82    Insert(Insert<K, V>),
83    Remove(Remove<K, V>),
84    Tag(Tag<T>),
85}
86
87impl<K: Clone + Hash + Eq, V: Clone, T: Hash + Eq + Clone> JournalEntry<K, V, T> {
88    pub fn reverse(&self, map: &mut HashMap<K, V>) {
89        match self {
90            Self::Insert(entry_data) => entry_data.reverse(map),
91            Self::Remove(entry_data) => entry_data.reverse(map),
92            Self::Tag(_) => {}
93        }
94    }
95
96    pub fn forward(&self, map: &mut HashMap<K, V>) {
97        match self {
98            Self::Insert(entry_data) => entry_data.forward(map),
99            Self::Remove(entry_data) => entry_data.forward(map),
100            Self::Tag(_) => {}
101        }
102    }
103}
104
105impl<K: Debug + Hash + Eq, V: Debug, T: Debug + Hash + Eq> std::fmt::Debug
106    for JournalEntry<K, V, T>
107{
108    fn fmt(&self, f: &mut std::fmt::Formatter) -> Result<(), std::fmt::Error> {
109        match self {
110            JournalEntry::Insert(entry) => {
111                write!(f, "{:?}", entry)
112            }
113            JournalEntry::Remove(entry) => {
114                write!(f, "{:?}", entry)
115            }
116            JournalEntry::Tag(entry) => {
117                write!(f, "{:?}", entry)
118            }
119        }
120    }
121}
122
123/// Journaling HashMap. Each transaction is recorded to the journal and can be
124/// undone and redone.
125///
126/// **NOTE:** Current implementation makes copies of all data, keys & tags.
127/// Memory usage is at least doubled, and if you're using data types which
128/// are large there can be a decent performance hit on write.
129///
130/// ## Usage
131///
132/// JournalMaps are designed to provide a simple interface to implement undo,
133/// redo and version control. This is achieved via recording all transactions
134/// in a journal and by adding "tags", which are basically just generic markers
135/// that can be used as undo checkpoints or version identifiers.
136///
137/// ```rust
138/// use journalmap::JournalMap;
139///
140/// fn main() {
141///     let mut ages: JournalMap<&'static str, i32, _> = JournalMap::new();
142///
143///     // Always add a tag at the start
144///     ages.append_tag("start").unwrap();
145///
146///     ages.insert("Sarah", 22);
147///     ages.insert("John", 21);
148///     ages.insert("Mariah", 45);
149///
150///     ages.append_tag("middle").unwrap();
151///
152///     ages.insert("Steve", 34);
153///     ages.insert("Carol", 41);
154///     ages.insert("Ellie", 21);
155///     ages.insert("Mariah", 87);
156///
157///     ages.append_tag("end").unwrap();
158///     
159///     // Currently all the entries we have are present...
160///     assert_eq!(ages.get(&"Sarah"), Some(&22));
161///     assert_eq!(ages.get(&"John"), Some(&21));
162///     assert_eq!(ages.get(&"Mariah"), Some(&87));
163///     assert_eq!(ages.get(&"Steve"), Some(&34));
164///     assert_eq!(ages.get(&"Carol"), Some(&41));
165///     assert_eq!(ages.get(&"Ellie"), Some(&21));
166///
167///     // Now let's reverse it and watch our inserts be undone...
168///     ages.reverse_to_tag(&"middle");
169///
170///     assert_eq!(ages.get(&"Sarah"), Some(&22));
171///     assert_eq!(ages.get(&"John"), Some(&21));
172///     assert_eq!(ages.get(&"Mariah"), Some(&45));
173///     assert_eq!(ages.contains_key(&"Steve"), false);
174///     assert_eq!(ages.contains_key(&"Carol"), false);
175///     assert_eq!(ages.contains_key(&"Ellie"), false);
176///
177///     // Inserting a new entry truncates the future so we can no longer
178///     // re-do the changes we've undone
179///     ages.insert("Omar", 33);
180///
181///     ages.append_tag("end").unwrap();
182///
183///
184///     assert_eq!(ages.get(&"Sarah"), Some(&22));
185///     assert_eq!(ages.get(&"John"), Some(&21));
186///     assert_eq!(ages.get(&"Mariah"), Some(&45));
187///     assert_eq!(ages.get(&"Omar"), Some(&33));
188///     assert_eq!(ages.contains_key(&"Steve"), false);
189///     assert_eq!(ages.contains_key(&"Carol"), false);
190///     assert_eq!(ages.contains_key(&"Ellie"), false);
191/// }
192/// ```
193pub struct JournalMap<K: Eq + Hash, V, T: Eq + Hash> {
194    journal: Vec<JournalEntry<K, V, T>>,
195    // It may be better to store the indices with the tags, but I will need to
196    // do benchmarking to figure this out. Storing tag indices may also over-
197    // complicate things too.
198    tags: HashSet<T>,
199    map: HashMap<K, V>,
200    current_tag_index: Option<usize>,
201}
202
203impl<K: Hash + Eq, V, T: Hash + Eq> Default for JournalMap<K, V, T> {
204    fn default() -> Self {
205        Self::new()
206    }
207}
208
209impl<K: Hash + Eq, V, T: Hash + Eq> JournalMap<K, V, T> {
210    pub fn new() -> Self {
211        Self {
212            journal: Vec::new(),
213            tags: HashSet::new(),
214            map: HashMap::new(),
215            current_tag_index: None,
216        }
217    }
218
219    pub fn with_capacity(capacity: usize) -> Self {
220        Self {
221            journal: Vec::with_capacity(capacity),
222            tags: HashSet::with_capacity(capacity),
223            map: HashMap::with_capacity(capacity),
224            current_tag_index: None,
225        }
226    }
227
228    #[inline]
229    pub fn len(&self) -> usize {
230        self.map.len()
231    }
232
233    #[inline]
234    pub fn journal_len(&self) -> usize {
235        self.journal.len()
236    }
237
238    #[inline]
239    pub fn is_empty(&self) -> bool {
240        self.map.is_empty()
241    }
242
243    pub fn current_tag(&self) -> Option<&T> {
244        let Some(i_tag) = self.current_tag_index else {
245            return None;
246        };
247
248        let JournalEntry::Tag(tag) = &self.journal[i_tag] else {
249            panic!("Tag not at tag index");
250        };
251
252        Some(&tag.tag)
253    }
254
255    #[inline]
256    pub fn as_hashmap(&self) -> &HashMap<K, V> {
257        &self.map
258    }
259
260    /// Clears the map and the journal.
261    pub fn clear_all(&mut self) {
262        self.map.clear();
263        self.journal.clear();
264        self.tags.clear();
265        self.current_tag_index = None;
266    }
267
268    /// Used to prepare for another journal entry. Truncates the journal past
269    /// the current tag, and clears the current tag index.
270    fn transaction_prep(&mut self) {
271        let Some(i_tag) = self.current_tag_index.take() else {
272            return;
273        };
274
275        let i_trunc = i_tag + 1;
276
277        for entry in self.journal[i_trunc..].iter() {
278            if let JournalEntry::Tag(tag) = entry {
279                self.tags.remove(&tag.tag);
280            }
281        }
282
283        self.journal.truncate(i_trunc);
284    }
285
286    /// Searches the journal in reverse from the current tag to find a tag
287    /// that matches the match function, returning the range to apply the
288    /// reverse function to. Also sets the current tag to the tag that matches
289    /// the match function.
290    fn i_tag_reverse_seek<MatchFn: Fn(&T) -> bool>(
291        &mut self,
292        match_fn: MatchFn,
293    ) -> Option<Range<usize>> {
294        let i_reverse_end = self.current_tag_index.unwrap_or(self.journal.len());
295
296        let Some(i_tag_rev_rel) = self.journal[..i_reverse_end]
297            .iter()
298            .rev()
299            .position(|entry| {
300                if let JournalEntry::Tag(tag) = entry
301                    && match_fn(&tag.tag)
302                {
303                    true
304                } else {
305                    false
306                }
307            })
308        else {
309            return None;
310        };
311
312        let i_tag = i_reverse_end - i_tag_rev_rel - 1;
313
314        self.current_tag_index = Some(i_tag);
315
316        let i_reverse_start = i_tag + 1;
317
318        Some(i_reverse_start..i_reverse_end)
319    }
320
321    /// Searches the journal in from the current tag to find a tag
322    /// that matches the match function, returning the range to apply the
323    /// reverse function to. Also sets the current tag to the tag that matches
324    /// the match function.
325    fn i_tag_forward_seek<MatchFn: Fn(&T) -> bool>(
326        &mut self,
327        match_fn: MatchFn,
328    ) -> Option<Range<usize>> {
329        let Some(i_current_tag) = self.current_tag_index else {
330            return None;
331        };
332
333        let i_forward_start = i_current_tag + 1;
334
335        let Some(i_next_tag_rel) = self.journal[i_forward_start..].iter().position(|entry| {
336            if let JournalEntry::Tag(tag) = entry
337                && match_fn(&tag.tag)
338            {
339                true
340            } else {
341                false
342            }
343        }) else {
344            return None;
345        };
346
347        let i_next_tag = i_forward_start + i_next_tag_rel;
348
349        self.current_tag_index = Some(i_next_tag);
350
351        let i_forward_end = i_next_tag;
352
353        Some(i_forward_start..i_forward_end)
354    }
355
356    /// Retain tags that match the criteria in the retain closure.
357    pub fn retain_tags<RetainFn: Fn(&T) -> bool>(&mut self, retain_fn: RetainFn) {
358        self.current_tag_index = None;
359
360        self.journal.retain(|entry| {
361            let JournalEntry::Tag(tag) = entry else {
362                return true;
363            };
364
365            let retain = retain_fn(&tag.tag);
366
367            if !retain {
368                self.tags.remove(&tag.tag);
369            }
370
371            retain
372        });
373    }
374}
375
376impl<K: Eq + Hash + Clone, V: Clone, T: Hash + Eq + Clone> JournalMap<K, V, T> {
377    /// Create a JournalMap set from an iterator. You can optionally place a tag
378    /// at the start of the journal with the `init_tag` argument.
379    pub fn from_iter<Iter: Iterator<Item = (K, V)>>(iter: Iter, init_tag: Option<T>) -> Self {
380        let map: HashMap<K, V> = HashMap::from_iter(iter);
381
382        let capacity = map.capacity() + init_tag.is_some() as usize;
383
384        let mut journal_map = Self {
385            journal: Vec::with_capacity(capacity),
386            tags: HashSet::with_capacity((capacity / 2).min(4)),
387            map,
388            current_tag_index: None,
389        };
390
391        if let Some(tag) = init_tag {
392            journal_map.tags.insert(tag.clone());
393
394            journal_map.journal.push(JournalEntry::Tag(Tag { tag }));
395        }
396
397        for (key, val) in journal_map.map.iter() {
398            journal_map.journal.push(JournalEntry::Insert(Insert {
399                key: key.clone(),
400                val: val.clone(),
401                replacing: None,
402            }));
403        }
404
405        journal_map
406    }
407
408    /// **NOTE:** Inserting makes up to 3 clones; a clone of the key, a clone
409    /// of the value, and a clone of the value being replaced if the key already
410    /// exists and is assigned something.
411    pub fn insert(&mut self, key: K, val: V) -> Option<V> {
412        self.transaction_prep();
413
414        let replacing = self.map.insert(key.clone(), val.clone());
415
416        let entry = JournalEntry::Insert(Insert {
417            key,
418            val,
419            replacing: replacing.clone(),
420        });
421
422        self.journal.push(entry);
423
424        replacing
425    }
426
427    pub fn get(&self, key: &K) -> Option<&V> {
428        self.map.get(key)
429    }
430
431    pub fn contains_key(&self, key: &K) -> bool {
432        self.map.contains_key(key)
433    }
434
435    pub fn remove(&mut self, key: K) -> Option<V> {
436        self.transaction_prep();
437
438        let value_opt = self.map.remove(&key);
439
440        if let Some(val) = value_opt.clone() {
441            self.journal.push(JournalEntry::Remove(Remove { key, val }));
442        }
443
444        value_opt
445    }
446
447    /// **NOTE:** Does not clear journal. Performs remove operations
448    /// on all items in the map, recording each. If you want to clear the map
449    /// and the journal, use `clear_all()`
450    pub fn clear(&mut self) {
451        self.transaction_prep();
452
453        for (key, val) in self.map.iter() {
454            self.journal.push(JournalEntry::Remove(Remove {
455                key: key.clone(),
456                val: val.clone(),
457            }));
458        }
459
460        self.map.clear();
461    }
462
463    /// Insert a tag at the current journal index. If the tag already exists,
464    /// return an error.
465    pub fn append_tag(&mut self, tag: T) -> Result<(), JournalError<T>> {
466        if self.tags.contains(&tag) {
467            return Err(JournalError::TagExists { tag });
468        }
469        self.current_tag_index = Some(self.journal.len());
470        self.journal
471            .push(JournalEntry::Tag(Tag { tag: tag.clone() }));
472        self.tags.insert(tag);
473
474        Ok(())
475    }
476
477    /// Replays the journal in reverse till the next tag is found. If no
478    /// tag is found, nothing happens.
479    pub fn reverse_to_next(&mut self) -> bool {
480        let Some(range) = self.i_tag_reverse_seek(|_| true) else {
481            return false;
482        };
483
484        if range.is_empty() {
485            return false;
486        }
487
488        self.reverse_range(range);
489
490        true
491    }
492
493    /// Replays the journal in reverse till the specified tag is found. If the
494    /// tag isn't found, nothing happens.
495    pub fn reverse_to_tag(&mut self, tag: &T) -> bool {
496        let Some(range) = self.i_tag_reverse_seek(|other_tag| other_tag == tag) else {
497            return false;
498        };
499
500        if range.is_empty() {
501            return false;
502        }
503
504        self.reverse_range(range);
505
506        true
507    }
508
509    /// Replays the journal till the next tag is found. If no
510    /// tag is found, nothing happens.
511    pub fn forward_to_next(&mut self) -> bool {
512        let Some(range) = self.i_tag_forward_seek(|_| true) else {
513            return false;
514        };
515
516        if range.is_empty() {
517            return false;
518        }
519
520        self.forward_range(range);
521
522        true
523    }
524
525    /// Replays the journal till the specified tag is found. If the
526    /// tag isn't found, nothing happens.
527    pub fn forward_to_tag(&mut self, tag: &T) -> bool {
528        let Some(range) = self.i_tag_forward_seek(|other_tag| other_tag == tag) else {
529            return false;
530        };
531
532        if range.is_empty() {
533            return false;
534        }
535
536        self.forward_range(range);
537
538        true
539    }
540
541    /// Replays the journal entries in the specified range in reverse
542    fn reverse_range(&mut self, range: Range<usize>) {
543        for entry in self.journal[range].iter().rev() {
544            entry.reverse(&mut self.map);
545        }
546    }
547
548    /// Replays the journal entries in the specified range
549    fn forward_range(&mut self, range: Range<usize>) {
550        for entry in self.journal[range].iter() {
551            entry.forward(&mut self.map);
552        }
553    }
554
555    /// Generates a journal struct based on the current state of the JournalMap.
556    ///
557    /// ### Flags:
558    /// * `truncate_future` - remove all journal entries past the currently
559    /// selected tag.
560    pub fn generate_journal(&self, truncate_future: bool) -> JournalMapJournal<K, V, T> {
561        let i_copy_start = 0;
562        let i_copy_end = if truncate_future && let Some(i_tag) = self.current_tag_index {
563            i_tag + 1
564        } else {
565            self.journal.len()
566        };
567
568        let journal_src = &self.journal[i_copy_start..i_copy_end];
569
570        let journal = journal_src.to_vec();
571
572        JournalMapJournal { journal }
573    }
574
575    /// Generated a JournalMap by replaying a JournalMapJournal from start to
576    /// finish.
577    pub fn from_journal(val: &JournalMapJournal<K, V, T>) -> Self {
578        let capacity = val.journal.len();
579        let map = HashMap::with_capacity(capacity);
580        let tags = HashSet::with_capacity(capacity);
581        let mut journal_map = Self {
582            map,
583            tags,
584            journal: val.journal.clone(),
585            current_tag_index: None,
586        };
587
588        for entry in journal_map.journal.iter() {
589            entry.forward(&mut journal_map.map);
590
591            if let JournalEntry::Tag(tag) = entry {
592                journal_map.tags.insert(tag.tag.clone());
593            }
594        }
595
596        journal_map
597    }
598}
599
600impl<K: Debug + Eq + Hash, V: Debug, T: Debug + Eq + Hash> std::fmt::Debug for JournalMap<K, V, T> {
601    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
602        let alternate = f.alternate();
603        write!(f, "{{")?;
604        for (i_entry, entry) in self.journal.iter().enumerate() {
605            if alternate {
606                write!(f, "\n\t")?;
607            }
608
609            if let Some(i_tag) = self.current_tag_index
610                && i_tag == i_entry
611            {
612                write!(f, "@ ")?;
613            }
614
615            write!(f, "{:?},", entry)?;
616        }
617
618        if alternate {
619            writeln!(f)?;
620        }
621
622        write!(f, "}}")?;
623
624        Ok(())
625    }
626}
627
628/// An extracted journal from a JournalMap. Used for storage purposes: if you
629/// want to serialize a JournalMap for later, you want to extract a
630/// JournalMapJournal and then serialize said journal.
631#[derive(Debug, Clone)]
632pub struct JournalMapJournal<K: Hash + Eq, V, T: Hash + Eq> {
633    journal: Vec<JournalEntry<K, V, T>>,
634}
635
636impl<K: Hash + Eq, V, T: Hash + Eq> JournalMapJournal<K, V, T> {
637    #[inline]
638    pub fn len(&self) -> usize {
639        self.journal.len()
640    }
641}
642
643#[cfg(feature = "serde")]
644impl<K: SerializeTrait + Hash + Eq, V: SerializeTrait, T: SerializeTrait + Eq + Hash> SerializeTrait
645    for JournalMapJournal<K, V, T>
646{
647    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
648        self.journal.serialize(serializer)
649    }
650}
651
652#[cfg(feature = "serde")]
653impl<
654    'de,
655    K: DeserializeTrait<'de> + Hash + Eq,
656    V: DeserializeTrait<'de>,
657    T: DeserializeTrait<'de> + Eq + Hash,
658> DeserializeTrait<'de> for JournalMapJournal<K, V, T>
659{
660    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
661        Ok(Self {
662            journal: Vec::deserialize(deserializer)?,
663        })
664    }
665}