#![forbid(unsafe_code)]
use std::collections::VecDeque;
use std::fmt;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct SnapshotConfig {
pub max_depth: usize,
}
impl Default for SnapshotConfig {
fn default() -> Self {
Self { max_depth: 100 }
}
}
impl SnapshotConfig {
#[must_use]
pub fn new(max_depth: usize) -> Self {
Self { max_depth }
}
#[must_use]
pub fn unlimited() -> Self {
Self {
max_depth: usize::MAX,
}
}
}
pub struct SnapshotStore<T> {
undo_stack: VecDeque<Arc<T>>,
redo_stack: VecDeque<Arc<T>>,
config: SnapshotConfig,
}
impl<T: fmt::Debug> fmt::Debug for SnapshotStore<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("SnapshotStore")
.field("undo_depth", &self.undo_stack.len())
.field("redo_depth", &self.redo_stack.len())
.field("config", &self.config)
.finish()
}
}
impl<T> SnapshotStore<T> {
#[must_use]
pub fn new(config: SnapshotConfig) -> Self {
Self {
undo_stack: VecDeque::new(),
redo_stack: VecDeque::new(),
config,
}
}
#[must_use]
pub fn with_default_config() -> Self {
Self::new(SnapshotConfig::default())
}
pub fn push(&mut self, state: T) {
self.redo_stack.clear();
self.undo_stack.push_back(Arc::new(state));
self.enforce_depth();
}
pub fn push_arc(&mut self, state: Arc<T>) {
self.redo_stack.clear();
self.undo_stack.push_back(state);
self.enforce_depth();
}
pub fn undo(&mut self) -> Option<Arc<T>> {
if self.undo_stack.len() < 2 {
return None;
}
let current = self.undo_stack.pop_back()?;
self.redo_stack.push_back(current);
self.undo_stack.back().cloned()
}
pub fn redo(&mut self) -> Option<Arc<T>> {
let snapshot = self.redo_stack.pop_back()?;
self.undo_stack.push_back(snapshot);
self.undo_stack.back().cloned()
}
#[must_use]
pub fn current(&self) -> Option<&Arc<T>> {
self.undo_stack.back()
}
#[must_use]
pub fn can_undo(&self) -> bool {
self.undo_stack.len() >= 2
}
#[must_use]
pub fn can_redo(&self) -> bool {
!self.redo_stack.is_empty()
}
#[must_use]
pub fn undo_depth(&self) -> usize {
self.undo_stack.len()
}
#[must_use]
pub fn redo_depth(&self) -> usize {
self.redo_stack.len()
}
#[must_use]
pub fn total_snapshots(&self) -> usize {
self.undo_stack.len() + self.redo_stack.len()
}
#[must_use]
pub fn config(&self) -> &SnapshotConfig {
&self.config
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.undo_stack.is_empty()
}
pub fn clear(&mut self) {
self.undo_stack.clear();
self.redo_stack.clear();
}
fn enforce_depth(&mut self) {
while self.undo_stack.len() > self.config.max_depth {
self.undo_stack.pop_front();
}
}
}
#[cfg(feature = "hamt")]
pub mod persistent {
pub use im::{HashMap, HashSet, OrdMap, OrdSet, Vector};
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn new_store_is_empty() {
let store = SnapshotStore::<i32>::with_default_config();
assert!(store.is_empty());
assert!(!store.can_undo());
assert!(!store.can_redo());
assert_eq!(store.undo_depth(), 0);
assert_eq!(store.redo_depth(), 0);
assert_eq!(store.total_snapshots(), 0);
assert!(store.current().is_none());
}
#[test]
fn push_makes_current_available() {
let mut store = SnapshotStore::with_default_config();
store.push(42);
assert!(!store.is_empty());
assert_eq!(**store.current().unwrap(), 42);
assert_eq!(store.undo_depth(), 1);
assert!(!store.can_undo()); }
#[test]
fn push_two_enables_undo() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
assert!(store.can_undo());
assert!(!store.can_redo());
assert_eq!(store.undo_depth(), 2);
}
#[test]
fn undo_restores_previous() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.push(3);
let prev = store.undo().unwrap();
assert_eq!(*prev, 2);
assert_eq!(**store.current().unwrap(), 2);
assert!(store.can_redo());
}
#[test]
fn undo_all_stops_at_initial() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.push(3);
assert!(store.undo().is_some()); assert!(store.undo().is_some()); assert!(store.undo().is_none()); assert_eq!(**store.current().unwrap(), 1);
}
#[test]
fn redo_restores_undone() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.undo();
let restored = store.redo().unwrap();
assert_eq!(*restored, 2);
assert_eq!(**store.current().unwrap(), 2);
assert!(!store.can_redo());
}
#[test]
fn push_clears_redo() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.undo();
assert!(store.can_redo());
store.push(3); assert!(!store.can_redo());
assert_eq!(store.redo_depth(), 0);
assert_eq!(**store.current().unwrap(), 3);
}
#[test]
fn redo_on_empty_returns_none() {
let mut store = SnapshotStore::<i32>::with_default_config();
assert!(store.redo().is_none());
}
#[test]
fn undo_on_empty_returns_none() {
let mut store = SnapshotStore::<i32>::with_default_config();
assert!(store.undo().is_none());
}
#[test]
fn undo_on_single_returns_none() {
let mut store = SnapshotStore::with_default_config();
store.push(42);
assert!(store.undo().is_none());
}
#[test]
fn depth_limit_evicts_oldest() {
let mut store = SnapshotStore::new(SnapshotConfig::new(3));
store.push(1);
store.push(2);
store.push(3);
store.push(4);
assert_eq!(store.undo_depth(), 3);
assert_eq!(**store.current().unwrap(), 4);
let prev = store.undo().unwrap();
assert_eq!(*prev, 3);
let prev = store.undo().unwrap();
assert_eq!(*prev, 2);
assert!(store.undo().is_none());
}
#[test]
fn depth_limit_one_keeps_only_latest() {
let mut store = SnapshotStore::new(SnapshotConfig::new(1));
store.push(1);
store.push(2);
store.push(3);
assert_eq!(store.undo_depth(), 1);
assert_eq!(**store.current().unwrap(), 3);
assert!(!store.can_undo());
}
#[test]
fn clear_removes_all() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.undo();
store.clear();
assert!(store.is_empty());
assert!(!store.can_undo());
assert!(!store.can_redo());
assert_eq!(store.total_snapshots(), 0);
}
#[test]
fn multiple_undo_redo_cycle() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.push(3);
store.undo(); store.undo();
assert_eq!(store.undo_depth(), 1);
assert_eq!(store.redo_depth(), 2);
store.redo(); store.redo();
assert_eq!(store.undo_depth(), 3);
assert_eq!(store.redo_depth(), 0);
assert_eq!(**store.current().unwrap(), 3);
}
#[test]
fn push_arc_avoids_double_wrap() {
let mut store = SnapshotStore::with_default_config();
let arc = Arc::new(42);
store.push_arc(arc.clone());
assert_eq!(**store.current().unwrap(), 42);
assert!(Arc::ptr_eq(store.current().unwrap(), &arc));
}
#[test]
fn structural_sharing_verified() {
let mut store = SnapshotStore::with_default_config();
let state = Arc::new(vec![1, 2, 3, 4, 5]);
store.push_arc(state.clone());
store.push_arc(state.clone());
let s1 = store.undo().unwrap();
let s2 = store.current().unwrap();
assert!(Arc::ptr_eq(&s1, s2));
}
#[test]
fn many_snapshots_within_memory() {
let mut store = SnapshotStore::new(SnapshotConfig::new(1000));
let data = Arc::new(vec![0u8; 1024]);
for _ in 0..1000 {
store.push_arc(data.clone());
}
assert_eq!(store.undo_depth(), 1000);
assert_eq!(Arc::strong_count(&data), 1001); }
#[test]
fn config_default() {
let config = SnapshotConfig::default();
assert_eq!(config.max_depth, 100);
}
#[test]
fn config_unlimited() {
let config = SnapshotConfig::unlimited();
assert_eq!(config.max_depth, usize::MAX);
}
#[test]
fn config_clone() {
let config = SnapshotConfig::new(50);
let cloned = config.clone();
assert_eq!(cloned.max_depth, 50);
}
#[test]
fn config_debug() {
let config = SnapshotConfig::new(42);
let s = format!("{config:?}");
assert!(s.contains("42"));
}
#[test]
fn store_debug() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
let s = format!("{store:?}");
assert!(s.contains("SnapshotStore"));
assert!(s.contains("undo_depth"));
}
#[test]
fn config_accessor() {
let store = SnapshotStore::<i32>::new(SnapshotConfig::new(42));
assert_eq!(store.config().max_depth, 42);
}
#[test]
fn total_snapshots_accounts_for_both_stacks() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.push(3);
assert_eq!(store.total_snapshots(), 3);
store.undo();
assert_eq!(store.total_snapshots(), 3);
store.undo();
assert_eq!(store.total_snapshots(), 3); }
#[test]
fn im_hashmap_structural_sharing() {
use im::HashMap;
let mut map = HashMap::new();
for i in 0..1000 {
map.insert(format!("key_{i}"), i);
}
let mut store = SnapshotStore::with_default_config();
store.push(map.clone());
let mut map2 = map.clone();
map2.insert("new_key".to_string(), 9999);
store.push(map2);
let prev = store.undo().unwrap();
assert_eq!(prev.len(), 1000);
assert!(!prev.contains_key("new_key"));
let restored = store.redo().unwrap();
assert_eq!(restored.len(), 1001);
assert_eq!(restored.get("new_key"), Some(&9999));
}
#[test]
fn im_vector_structural_sharing() {
use im::Vector;
let mut vec: Vector<u32> = (0..1000).collect();
let mut store = SnapshotStore::with_default_config();
store.push(vec.clone());
vec.push_back(9999);
store.push(vec);
let prev = store.undo().unwrap();
assert_eq!(prev.len(), 1000);
let restored = store.redo().unwrap();
assert_eq!(restored.len(), 1001);
assert_eq!(restored.back(), Some(&9999));
}
#[test]
fn im_hashmap_many_snapshots_memory_efficiency() {
use im::HashMap;
let mut state: HashMap<String, Vec<u8>> = HashMap::new();
for i in 0..100 {
state.insert(format!("entry_{i}"), vec![0u8; 100]);
}
let mut store = SnapshotStore::new(SnapshotConfig::new(1000));
for i in 0..50 {
store.push(state.clone());
state.insert(format!("entry_{}", i % 100), vec![i as u8; 100]);
}
assert_eq!(store.undo_depth(), 50);
for _ in 0..49 {
let prev = store.undo().unwrap();
assert_eq!(prev.len(), 100);
}
assert!(store.undo().is_none());
}
#[test]
fn depth_limit_zero_evicts_everything() {
let mut store = SnapshotStore::new(SnapshotConfig::new(0));
store.push(42);
assert!(store.is_empty());
}
#[test]
fn push_arc_clears_redo() {
let mut store = SnapshotStore::with_default_config();
store.push(1);
store.push(2);
store.undo();
assert!(store.can_redo());
store.push_arc(Arc::new(3));
assert!(!store.can_redo());
}
#[test]
fn undo_redo_returns_correct_arc() {
let mut store = SnapshotStore::with_default_config();
store.push("a");
store.push("b");
store.push("c");
let undone = store.undo().unwrap();
assert_eq!(*undone, "b");
let redone = store.redo().unwrap();
assert_eq!(*redone, "c");
}
}