Skip to main content

okbot_testkit/
memory_tasks.rs

1//! An in-memory [`TaskStore`].
2
3use std::sync::{Arc, Mutex};
4
5use okbot_core::{
6    Clock, NewTask, Requester, Task, TaskChange, TaskError, TaskStatus, TaskStore, async_trait,
7    sort_tasks,
8};
9
10/// A [`TaskStore`] kept in memory, for runtime tests: every requester sees every task, ids are
11/// `tasks/test/t-<n>`, and no rule is checked.
12pub struct MemoryTasks {
13    tasks: Mutex<Vec<Task>>,
14    clock: Arc<dyn Clock>,
15}
16
17impl std::fmt::Debug for MemoryTasks {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        f.debug_struct("MemoryTasks").finish_non_exhaustive()
20    }
21}
22
23impl MemoryTasks {
24    /// An empty store reading time from `clock`.
25    pub fn new(clock: Arc<dyn Clock>) -> Self {
26        Self {
27            tasks: Mutex::new(Vec::new()),
28            clock,
29        }
30    }
31
32    fn lock(&self) -> std::sync::MutexGuard<'_, Vec<Task>> {
33        self.tasks
34            .lock()
35            .unwrap_or_else(std::sync::PoisonError::into_inner)
36    }
37
38    /// Every task, in display order.
39    pub fn all(&self) -> Vec<Task> {
40        let mut tasks = self.lock().clone();
41        sort_tasks(&mut tasks);
42        tasks
43    }
44
45    /// Adds a fully built task as it is (for tests that need a past time or an origin).
46    pub fn insert(&self, task: Task) {
47        self.lock().push(task);
48    }
49
50    fn change(&self, id: &str, change: TaskChange) -> Result<Task, TaskError> {
51        let mut tasks = self.lock();
52        let task = tasks
53            .iter_mut()
54            .find(|t| t.id == id || t.id.rsplit('/').next() == Some(id))
55            .ok_or_else(|| TaskError::NotFound(id.to_string()))?;
56        if let Some(title) = change.title {
57            task.title = title;
58        }
59        if let Some(notes) = change.notes {
60            task.notes = notes;
61        }
62        if let Some(status) = change.status {
63            task.status = status;
64        }
65        if let Some(due) = change.due {
66            task.due = due;
67        }
68        if let Some(at) = change.remind_at {
69            task.remind_at = at;
70        }
71        if let Some(repeat) = change.repeat {
72            task.repeat = repeat;
73        }
74        Ok(task.clone())
75    }
76}
77
78#[async_trait]
79impl TaskStore for MemoryTasks {
80    async fn add(&self, who: &Requester, new: NewTask) -> Result<Task, TaskError> {
81        let mut tasks = self.lock();
82        let task = Task {
83            id: format!("tasks/test/t-{}", tasks.len() + 1),
84            kind: new.kind,
85            status: if new.paused {
86                TaskStatus::Paused
87            } else {
88                TaskStatus::Open
89            },
90            title: new.title,
91            notes: new.notes,
92            scope: new.scope,
93            due: new.due,
94            remind_at: new.remind_at,
95            repeat: new.repeat,
96            deliver_to: new.deliver_to,
97            origin: Some(who.conversation.clone()),
98            author: format!("{}/{}", who.conversation.channel, who.user.id),
99            created: self.clock.now(),
100        };
101        tasks.push(task.clone());
102        Ok(task)
103    }
104
105    async fn list(&self, _who: &Requester) -> Result<Vec<Task>, TaskError> {
106        Ok(self.all())
107    }
108
109    async fn update(
110        &self,
111        _who: &Requester,
112        id: &str,
113        change: TaskChange,
114    ) -> Result<Task, TaskError> {
115        self.change(id, change)
116    }
117
118    async fn delete(&self, _who: &Requester, id: &str) -> Result<Task, TaskError> {
119        let mut tasks = self.lock();
120        let at = tasks
121            .iter()
122            .position(|t| t.id == id || t.id.rsplit('/').next() == Some(id))
123            .ok_or_else(|| TaskError::NotFound(id.to_string()))?;
124        Ok(tasks.remove(at))
125    }
126
127    async fn scheduled(&self) -> Result<Vec<Task>, TaskError> {
128        Ok(self
129            .all()
130            .into_iter()
131            .filter(|t| t.status.is_active() && t.remind_at.is_some())
132            .collect())
133    }
134
135    async fn after_fire(&self, id: &str, change: TaskChange) -> Result<Task, TaskError> {
136        self.change(id, change)
137    }
138}