1use std::collections::HashMap;
10use std::path::{Path, PathBuf};
11
12#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
14pub struct MemoryEntry {
15 pub key: String,
16 pub value: String,
17 pub timestamp: u64,
18 pub source_step: String,
19}
20
21#[derive(Debug)]
23pub struct SessionStore {
24 memory: HashMap<String, MemoryEntry>,
26 store: HashMap<String, MemoryEntry>,
28 store_path: PathBuf,
30 dirty: bool,
32}
33
34impl SessionStore {
35 pub fn new(source_file: &str) -> Self {
37 let store_path = Self::store_path_for(source_file);
38 let store = Self::load_store(&store_path);
39
40 SessionStore {
41 memory: HashMap::new(),
42 store,
43 store_path,
44 dirty: false,
45 }
46 }
47
48 fn store_path_for(source_file: &str) -> PathBuf {
50 let p = Path::new(source_file);
51 let stem = p.file_stem().unwrap_or_default().to_string_lossy();
52 let dir = p.parent().unwrap_or_else(|| Path::new("."));
53 dir.join(format!(".{stem}.session.json"))
54 }
55
56 fn load_store(path: &Path) -> HashMap<String, MemoryEntry> {
58 match std::fs::read_to_string(path) {
59 Ok(json) => {
60 let entries: Vec<MemoryEntry> = serde_json::from_str(&json).unwrap_or_default();
61 entries.into_iter().map(|e| (e.key.clone(), e)).collect()
62 }
63 Err(_) => HashMap::new(),
64 }
65 }
66
67 pub fn flush(&self) -> Result<(), String> {
69 if !self.dirty {
70 return Ok(());
71 }
72 let entries: Vec<&MemoryEntry> = self.store.values().collect();
73 let json = serde_json::to_string_pretty(&entries)
74 .map_err(|e| format!("Failed to serialize session store: {e}"))?;
75 std::fs::write(&self.store_path, json)
76 .map_err(|e| format!("Failed to write session store: {e}"))?;
77 Ok(())
78 }
79
80 pub fn remember(&mut self, key: &str, value: &str, source_step: &str) {
84 let entry = MemoryEntry {
85 key: key.to_string(),
86 value: value.to_string(),
87 timestamp: current_timestamp(),
88 source_step: source_step.to_string(),
89 };
90 self.memory.insert(key.to_string(), entry);
91 }
92
93 pub fn recall(&self, key: &str) -> Option<&MemoryEntry> {
95 self.memory.get(key)
96 }
97
98 pub fn memory_entries(&self) -> Vec<&MemoryEntry> {
100 self.memory.values().collect()
101 }
102
103 pub fn persist(&mut self, key: &str, value: &str, source_step: &str) {
107 let entry = MemoryEntry {
108 key: key.to_string(),
109 value: value.to_string(),
110 timestamp: current_timestamp(),
111 source_step: source_step.to_string(),
112 };
113 self.store.insert(key.to_string(), entry);
114 self.dirty = true;
115 }
116
117 pub fn retrieve(&self, key: &str) -> Option<&MemoryEntry> {
119 self.store.get(key)
120 }
121
122 pub fn retrieve_query(&self, query: &str) -> Vec<&MemoryEntry> {
124 let q = query.to_lowercase();
125 self.store
126 .values()
127 .filter(|e| e.key.to_lowercase().contains(&q) || e.value.to_lowercase().contains(&q))
128 .collect()
129 }
130
131 pub fn mutate(&mut self, key: &str, new_value: &str, source_step: &str) -> bool {
134 if self.store.contains_key(key) {
135 self.persist(key, new_value, source_step);
136 true
137 } else {
138 false
139 }
140 }
141
142 pub fn purge(&mut self, key: &str) -> bool {
145 if self.store.remove(key).is_some() {
146 self.dirty = true;
147 true
148 } else {
149 false
150 }
151 }
152
153 pub fn purge_query(&mut self, query: &str) -> usize {
156 let q = query.to_lowercase();
157 let keys_to_remove: Vec<String> = self
158 .store
159 .iter()
160 .filter(|(_, e)| e.key.to_lowercase().contains(&q) || e.value.to_lowercase().contains(&q))
161 .map(|(k, _)| k.clone())
162 .collect();
163 let count = keys_to_remove.len();
164 for k in keys_to_remove {
165 self.store.remove(&k);
166 }
167 if count > 0 {
168 self.dirty = true;
169 }
170 count
171 }
172
173 pub fn store_count(&self) -> usize {
175 self.store.len()
176 }
177
178 pub fn memory_count(&self) -> usize {
180 self.memory.len()
181 }
182
183 pub fn store_path(&self) -> &Path {
185 &self.store_path
186 }
187}
188
189fn current_timestamp() -> u64 {
190 std::time::SystemTime::now()
191 .duration_since(std::time::UNIX_EPOCH)
192 .unwrap_or_default()
193 .as_secs()
194}
195
196#[cfg(test)]
199mod tests {
200 use super::*;
201
202 fn temp_store(name: &str) -> SessionStore {
203 let tmp = std::env::temp_dir().join(format!("axon_test_{name}.axon"));
204 SessionStore::new(tmp.to_str().unwrap())
205 }
206
207 #[test]
208 fn remember_and_recall() {
209 let mut store = temp_store("rem_recall");
210 store.remember("key1", "value1", "step_a");
211 let entry = store.recall("key1").unwrap();
212 assert_eq!(entry.value, "value1");
213 assert_eq!(entry.source_step, "step_a");
214 assert!(store.recall("nonexistent").is_none());
215 }
216
217 #[test]
218 fn remember_overwrites() {
219 let mut store = temp_store("rem_overwrite");
220 store.remember("k", "v1", "s1");
221 store.remember("k", "v2", "s2");
222 assert_eq!(store.recall("k").unwrap().value, "v2");
223 }
224
225 #[test]
226 fn persist_and_retrieve() {
227 let mut store = temp_store("persist_ret");
228 store.persist("data", "hello world", "persist_step");
229 let entry = store.retrieve("data").unwrap();
230 assert_eq!(entry.value, "hello world");
231 assert!(store.retrieve("missing").is_none());
232 }
233
234 #[test]
235 fn retrieve_query_matches() {
236 let mut store = temp_store("ret_query");
237 store.persist("analysis_result", "the answer is 42", "s1");
238 store.persist("user_pref", "dark mode", "s2");
239 store.persist("analysis_notes", "see appendix", "s3");
240
241 let results = store.retrieve_query("analysis");
242 assert_eq!(results.len(), 2);
243 }
244
245 #[test]
246 fn mutate_existing() {
247 let mut store = temp_store("mutate");
248 store.persist("k", "old", "s1");
249 assert!(store.mutate("k", "new", "s2"));
250 assert_eq!(store.retrieve("k").unwrap().value, "new");
251 }
252
253 #[test]
254 fn mutate_missing_returns_false() {
255 let mut store = temp_store("mutate_miss");
256 assert!(!store.mutate("nope", "val", "s1"));
257 }
258
259 #[test]
260 fn purge_existing() {
261 let mut store = temp_store("purge");
262 store.persist("k", "v", "s1");
263 assert!(store.purge("k"));
264 assert!(store.retrieve("k").is_none());
265 }
266
267 #[test]
268 fn purge_missing_returns_false() {
269 let mut store = temp_store("purge_miss");
270 assert!(!store.purge("nope"));
271 }
272
273 #[test]
274 fn purge_query_removes_matching() {
275 let mut store = temp_store("purge_q");
276 store.persist("temp_a", "x", "s1");
277 store.persist("temp_b", "y", "s2");
278 store.persist("keep_c", "z", "s3");
279 let removed = store.purge_query("temp");
280 assert_eq!(removed, 2);
281 assert_eq!(store.store_count(), 1);
282 }
283
284 #[test]
285 fn flush_and_reload() {
286 let tmp = std::env::temp_dir().join("axon_test_flush.axon");
287 let store_path = {
288 let mut store = SessionStore::new(tmp.to_str().unwrap());
289 store.persist("persistent_key", "persistent_value", "test");
290 store.flush().unwrap();
291 store.store_path().to_path_buf()
292 };
293
294 let store2 = SessionStore::new(tmp.to_str().unwrap());
296 let entry = store2.retrieve("persistent_key").unwrap();
297 assert_eq!(entry.value, "persistent_value");
298
299 let _ = std::fs::remove_file(&store_path);
301 }
302
303 #[test]
304 fn memory_count_and_store_count() {
305 let mut store = temp_store("counts");
306 store.remember("a", "1", "s");
307 store.remember("b", "2", "s");
308 store.persist("x", "10", "s");
309 assert_eq!(store.memory_count(), 2);
310 assert_eq!(store.store_count(), 1);
311 }
312
313 #[test]
314 fn store_path_derives_from_source() {
315 let store = SessionStore::new("/path/to/myprogram.axon");
316 let path_str = store.store_path().to_string_lossy();
317 assert!(path_str.contains(".myprogram.session.json"));
318 }
319
320 #[test]
321 fn timestamp_is_recent() {
322 let mut store = temp_store("timestamp");
323 store.remember("k", "v", "s");
324 let ts = store.recall("k").unwrap().timestamp;
325 assert!(ts > 1700000000); }
327}