Skip to main content

agentstategraph_reminders/
memory.rs

1//! In-process memory store — for tests and lightweight consumers that
2//! do not need durable reminder storage.
3
4use std::collections::HashMap;
5use std::sync::RwLock;
6
7use crate::error::ReminderError;
8use crate::store::ReminderStore;
9use crate::types::{Reminder, ReminderFilter};
10
11pub struct MemoryReminderStore {
12    reminders: RwLock<HashMap<String, Reminder>>,
13}
14
15impl Default for MemoryReminderStore {
16    fn default() -> Self {
17        Self::new()
18    }
19}
20
21impl MemoryReminderStore {
22    pub fn new() -> Self {
23        Self {
24            reminders: RwLock::new(HashMap::new()),
25        }
26    }
27}
28
29impl ReminderStore for MemoryReminderStore {
30    fn save(&self, reminder: &Reminder) -> Result<(), ReminderError> {
31        self.reminders
32            .write()
33            .expect("MemoryReminderStore lock poisoned")
34            .insert(reminder.id.clone(), reminder.clone());
35        Ok(())
36    }
37
38    fn get(&self, id: &str) -> Result<Option<Reminder>, ReminderError> {
39        Ok(self
40            .reminders
41            .read()
42            .expect("MemoryReminderStore lock poisoned")
43            .get(id)
44            .cloned())
45    }
46
47    fn update(&self, reminder: &Reminder) -> Result<(), ReminderError> {
48        let mut map = self
49            .reminders
50            .write()
51            .expect("MemoryReminderStore lock poisoned");
52        if map.contains_key(&reminder.id) {
53            map.insert(reminder.id.clone(), reminder.clone());
54            Ok(())
55        } else {
56            Err(ReminderError::NotFound(reminder.id.clone()))
57        }
58    }
59
60    fn update_if_unchanged(
61        &self,
62        expected: &Reminder,
63        new: &Reminder,
64    ) -> Result<bool, ReminderError> {
65        let mut map = self
66            .reminders
67            .write()
68            .expect("MemoryReminderStore lock poisoned");
69        match map.get(&expected.id) {
70            Some(current) if crate::store::same_reminder(current, expected) => {
71                map.insert(new.id.clone(), new.clone());
72                Ok(true)
73            }
74            Some(_) => Ok(false),
75            None => Err(ReminderError::NotFound(expected.id.clone())),
76        }
77    }
78
79    fn delete(&self, id: &str) -> Result<bool, ReminderError> {
80        Ok(self
81            .reminders
82            .write()
83            .expect("MemoryReminderStore lock poisoned")
84            .remove(id)
85            .is_some())
86    }
87
88    fn list(&self, filter: &ReminderFilter) -> Result<Vec<Reminder>, ReminderError> {
89        let map = self
90            .reminders
91            .read()
92            .expect("MemoryReminderStore lock poisoned");
93        let mut results: Vec<Reminder> = map
94            .values()
95            .filter(|r| filter.matches(r))
96            .cloned()
97            .collect();
98        // Sort by priority (ascending = most urgent first), then due_at ascending.
99        results.sort_by(|a, b| a.priority.cmp(&b.priority).then(a.due_at.cmp(&b.due_at)));
100        Ok(results)
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use crate::types::{CreateReminder, Priority, ReminderStatus};
108    use chrono::{Duration, Utc};
109
110    fn make(title: &str, priority: Priority, due_secs: i64) -> Reminder {
111        CreateReminder::new(
112            title,
113            "instructions",
114            Utc::now() + Duration::seconds(due_secs),
115            "agent/test",
116        )
117        .with_priority(priority)
118        .into_reminder()
119    }
120
121    #[test]
122    fn save_and_get_roundtrip() {
123        let store = MemoryReminderStore::new();
124        let r = make("test", Priority::Medium, 60);
125        store.save(&r).unwrap();
126        let fetched = store.get(&r.id).unwrap().unwrap();
127        assert_eq!(fetched.id, r.id);
128        assert_eq!(fetched.title, "test");
129    }
130
131    #[test]
132    fn get_nonexistent_returns_none() {
133        let store = MemoryReminderStore::new();
134        assert!(store.get("no-such-id").unwrap().is_none());
135    }
136
137    #[test]
138    fn update_mutates_record() {
139        let store = MemoryReminderStore::new();
140        let mut r = make("original", Priority::Low, 60);
141        store.save(&r).unwrap();
142        r.title = "updated".into();
143        store.update(&r).unwrap();
144        assert_eq!(store.get(&r.id).unwrap().unwrap().title, "updated");
145    }
146
147    #[test]
148    fn update_nonexistent_returns_not_found() {
149        let store = MemoryReminderStore::new();
150        let r = make("x", Priority::Low, 60);
151        assert!(matches!(store.update(&r), Err(ReminderError::NotFound(_))));
152    }
153
154    #[test]
155    fn delete_returns_true_on_success_false_when_missing() {
156        let store = MemoryReminderStore::new();
157        let r = make("x", Priority::Low, 60);
158        store.save(&r).unwrap();
159        assert!(store.delete(&r.id).unwrap());
160        assert!(!store.delete(&r.id).unwrap());
161    }
162
163    #[test]
164    fn list_ordered_by_priority_then_due_at() {
165        let store = MemoryReminderStore::new();
166        let a = make("low-later", Priority::Low, 120);
167        let b = make("high-later", Priority::High, 120);
168        let c = make("high-sooner", Priority::High, 60);
169        for r in [&a, &b, &c] {
170            store.save(r).unwrap();
171        }
172
173        let results = store.list(&ReminderFilter::default()).unwrap();
174        assert_eq!(results[0].title, "high-sooner");
175        assert_eq!(results[1].title, "high-later");
176        assert_eq!(results[2].title, "low-later");
177    }
178
179    #[test]
180    fn list_filters_by_status() {
181        let store = MemoryReminderStore::new();
182        let mut r1 = make("due", Priority::Medium, -10);
183        r1.status = ReminderStatus::Due;
184        let r2 = make("pending", Priority::Medium, 60);
185        store.save(&r1).unwrap();
186        store.save(&r2).unwrap();
187
188        let results = store
189            .list(&ReminderFilter {
190                status: Some(ReminderStatus::Due),
191                ..Default::default()
192            })
193            .unwrap();
194        assert_eq!(results.len(), 1);
195        assert_eq!(results[0].title, "due");
196    }
197}