logisheets_controller/
checkpoint_manager.rs1use std::collections::VecDeque;
20
21use logisheets_base::errors::BasicError;
22
23use crate::controller::status::Status;
24
25const MAX_CHECKPOINTS: usize = 20;
33
34#[derive(Debug, Clone)]
36pub struct CheckpointEntry {
37 pub label: String,
38 pub description: Option<String>,
41 pub status: Status,
44}
45
46#[derive(Debug)]
48pub struct CheckpointManager {
49 entries: VecDeque<CheckpointEntry>,
53}
54
55impl Default for CheckpointManager {
56 fn default() -> Self {
57 Self {
58 entries: VecDeque::new(),
59 }
60 }
61}
62
63impl CheckpointManager {
64 pub fn save(
69 &mut self,
70 label: String,
71 description: Option<String>,
72 status: Status,
73 ) -> &CheckpointEntry {
74 self.entries.retain(|e| e.label != label);
76
77 while self.entries.len() >= MAX_CHECKPOINTS {
79 self.entries.pop_back();
80 }
81
82 self.entries.push_front(CheckpointEntry {
83 label,
84 description,
85 status,
86 });
87 self.entries.front().expect("just pushed")
88 }
89
90 pub fn get(&self, label: &str) -> Result<&Status, BasicError> {
94 self.entries
95 .iter()
96 .find(|e| e.label == label)
97 .map(|e| &e.status)
98 .ok_or_else(|| BasicError::CheckpointNotFound(label.to_string()))
99 }
100
101 pub fn delete(&mut self, label: &str) -> bool {
103 let before = self.entries.len();
104 self.entries.retain(|e| e.label != label);
105 self.entries.len() != before
106 }
107
108 pub fn list(&self) -> Vec<CheckpointMeta> {
111 self.entries
112 .iter()
113 .map(|e| CheckpointMeta {
114 label: e.label.clone(),
115 description: e.description.clone(),
116 })
117 .collect()
118 }
119
120 pub fn len(&self) -> usize {
122 self.entries.len()
123 }
124
125 pub fn is_empty(&self) -> bool {
127 self.entries.is_empty()
128 }
129
130 pub fn clear(&mut self) {
133 self.entries.clear();
134 }
135}
136
137#[derive(Debug, Clone)]
140pub struct CheckpointMeta {
141 pub label: String,
142 pub description: Option<String>,
143}
144
145#[cfg(test)]
146mod tests {
147 use super::*;
148
149 fn empty_status() -> Status {
150 Status::default()
151 }
152
153 #[test]
154 fn save_then_get_returns_snapshot() {
155 let mut m = CheckpointManager::default();
156 m.save("a".into(), Some("first".into()), empty_status());
157 assert!(m.get("a").is_ok());
158 }
159
160 #[test]
161 fn get_missing_returns_err() {
162 let m = CheckpointManager::default();
163 let err = m.get("nope").unwrap_err();
164 match err {
165 BasicError::CheckpointNotFound(s) => assert_eq!(s, "nope"),
166 _ => panic!("expected CheckpointNotFound"),
167 }
168 }
169
170 #[test]
171 fn save_with_existing_label_overwrites() {
172 let mut m = CheckpointManager::default();
173 m.save("a".into(), Some("v1".into()), empty_status());
174 m.save("a".into(), Some("v2".into()), empty_status());
175 assert_eq!(m.len(), 1);
176 assert_eq!(m.list()[0].description.as_deref(), Some("v2"));
177 }
178
179 #[test]
180 fn list_is_newest_first() {
181 let mut m = CheckpointManager::default();
182 m.save("a".into(), None, empty_status());
183 m.save("b".into(), None, empty_status());
184 m.save("c".into(), None, empty_status());
185 let labels: Vec<_> = m.list().iter().map(|e| e.label.clone()).collect();
186 assert_eq!(labels, vec!["c", "b", "a"]);
187 }
188
189 #[test]
190 fn delete_removes_entry() {
191 let mut m = CheckpointManager::default();
192 m.save("a".into(), None, empty_status());
193 assert!(m.delete("a"));
194 assert!(!m.delete("a")); assert!(m.is_empty());
196 }
197
198 #[test]
199 fn eviction_drops_oldest_when_full() {
200 let mut m = CheckpointManager::default();
201 for i in 0..(MAX_CHECKPOINTS + 5) {
202 m.save(format!("k{}", i), None, empty_status());
203 }
204 assert_eq!(m.len(), MAX_CHECKPOINTS);
205 assert!(m.get("k0").is_err());
207 assert!(m.get("k4").is_err());
208 assert!(m.get("k5").is_ok());
209 assert!(m.get(&format!("k{}", MAX_CHECKPOINTS + 4)).is_ok());
211 }
212
213 #[test]
214 fn resave_moves_to_front_so_no_eviction() {
215 let mut m = CheckpointManager::default();
216 m.save("keep".into(), None, empty_status());
217 for i in 0..MAX_CHECKPOINTS {
218 m.save(format!("k{}", i), None, empty_status());
219 if i % 2 == 0 {
222 m.save("keep".into(), None, empty_status());
223 }
224 }
225 assert!(m.get("keep").is_ok());
226 }
227}