Skip to main content

frp_loom/
memory.rs

1//! In-memory [`AtomStore`], [`BlockStore`], and [`EdgeStore`] implementations.
2//!
3//! Enabled via the `in-memory` Cargo feature. These are backed by
4//! `HashMap` and are suitable for tests, prototyping, and single-process
5//! deployments that don't require persistence.
6
7use std::collections::HashMap;
8
9use frp_plexus::{AtomId, BlockId, EdgeId};
10
11use crate::error::StoreError;
12use crate::query::{Query, QueryResult};
13use crate::store::{AtomStore, BlockStore, EdgeStore};
14
15// ---------------------------------------------------------------------------
16// Helper: apply limit + offset pagination to an iterator of references
17// ---------------------------------------------------------------------------
18
19fn paginate<'a, T>(
20    iter: impl Iterator<Item = &'a T>,
21    total: usize,
22    query: &Query,
23) -> QueryResult<&'a T> {
24    let paged: Vec<&T> = iter
25        .skip(query.offset)
26        .take(query.limit.unwrap_or(usize::MAX))
27        .collect();
28    QueryResult::new(paged, total, query.offset)
29}
30
31// ---------------------------------------------------------------------------
32// InMemoryAtomStore
33// ---------------------------------------------------------------------------
34
35/// A `HashMap`-backed [`AtomStore`] for any `Clone` atom type.
36///
37/// Kind/tag filtering in [`AtomStore::query_atoms`] is deferred to the
38/// domain layer — this implementation returns all atoms subject only to
39/// limit/offset pagination.
40#[derive(Debug, Default)]
41pub struct InMemoryAtomStore<V: Clone> {
42    data: HashMap<AtomId, V>,
43}
44
45impl<V: Clone> InMemoryAtomStore<V> {
46    /// Create an empty store.
47    pub fn new() -> Self {
48        Self { data: HashMap::new() }
49    }
50
51    /// Number of atoms currently stored.
52    pub fn len(&self) -> usize {
53        self.data.len()
54    }
55
56    /// Returns `true` if the store is empty.
57    pub fn is_empty(&self) -> bool {
58        self.data.is_empty()
59    }
60}
61
62/// Retrieve the `AtomId` from a value. Implement this on your domain `Atom`
63/// type so that [`InMemoryAtomStore::put_atom`] can derive the key.
64pub trait HasAtomId {
65    fn atom_id(&self) -> AtomId;
66}
67
68impl<V: Clone + HasAtomId> AtomStore for InMemoryAtomStore<V> {
69    type Atom = V;
70
71    fn get_atom(&self, id: AtomId) -> Result<&V, StoreError> {
72        self.data.get(&id).ok_or_else(|| StoreError::not_found(id))
73    }
74
75    fn put_atom(&mut self, atom: V) -> Result<(), StoreError> {
76        let id = atom.atom_id();
77        self.data.insert(id, atom);
78        Ok(())
79    }
80
81    fn delete_atom(&mut self, id: AtomId) -> Result<(), StoreError> {
82        self.data.remove(&id).ok_or_else(|| StoreError::not_found(id))?;
83        Ok(())
84    }
85
86    fn query_atoms(&self, query: &Query) -> Result<QueryResult<&V>, StoreError> {
87        let all: Vec<&V> = self.data.values().collect();
88        let total = all.len();
89        Ok(paginate(all.into_iter(), total, query))
90    }
91}
92
93// ---------------------------------------------------------------------------
94// InMemoryBlockStore
95// ---------------------------------------------------------------------------
96
97/// A `HashMap`-backed [`BlockStore`] for any `Clone` block type.
98#[derive(Debug, Default)]
99pub struct InMemoryBlockStore<V: Clone> {
100    data: HashMap<BlockId, V>,
101}
102
103impl<V: Clone> InMemoryBlockStore<V> {
104    /// Create an empty store.
105    pub fn new() -> Self {
106        Self { data: HashMap::new() }
107    }
108
109    /// Number of blocks currently stored.
110    pub fn len(&self) -> usize {
111        self.data.len()
112    }
113
114    /// Returns `true` if the store is empty.
115    pub fn is_empty(&self) -> bool {
116        self.data.is_empty()
117    }
118}
119
120/// Retrieve the `BlockId` from a value.
121pub trait HasBlockId {
122    fn block_id(&self) -> BlockId;
123}
124
125impl<V: Clone + HasBlockId> BlockStore for InMemoryBlockStore<V> {
126    type Block = V;
127
128    fn get_block(&self, id: BlockId) -> Result<&V, StoreError> {
129        self.data.get(&id).ok_or_else(|| StoreError::not_found(id))
130    }
131
132    fn put_block(&mut self, block: V) -> Result<(), StoreError> {
133        let id = block.block_id();
134        self.data.insert(id, block);
135        Ok(())
136    }
137
138    fn delete_block(&mut self, id: BlockId) -> Result<(), StoreError> {
139        self.data.remove(&id).ok_or_else(|| StoreError::not_found(id))?;
140        Ok(())
141    }
142
143    fn query_blocks(&self, query: &Query) -> Result<QueryResult<&V>, StoreError> {
144        let all: Vec<&V> = self.data.values().collect();
145        let total = all.len();
146        Ok(paginate(all.into_iter(), total, query))
147    }
148}
149
150// ---------------------------------------------------------------------------
151// InMemoryEdgeStore
152// ---------------------------------------------------------------------------
153
154/// A `HashMap`-backed [`EdgeStore`] for any `Clone` edge type.
155#[derive(Debug, Default)]
156pub struct InMemoryEdgeStore<V: Clone> {
157    data: HashMap<EdgeId, V>,
158}
159
160impl<V: Clone> InMemoryEdgeStore<V> {
161    /// Create an empty store.
162    pub fn new() -> Self {
163        Self { data: HashMap::new() }
164    }
165
166    /// Number of edges currently stored.
167    pub fn len(&self) -> usize {
168        self.data.len()
169    }
170
171    /// Returns `true` if the store is empty.
172    pub fn is_empty(&self) -> bool {
173        self.data.is_empty()
174    }
175}
176
177/// Retrieve the `EdgeId` from a value.
178pub trait HasEdgeId {
179    fn edge_id(&self) -> EdgeId;
180}
181
182impl<V: Clone + HasEdgeId> EdgeStore for InMemoryEdgeStore<V> {
183    type Edge = V;
184
185    fn get_edge(&self, id: EdgeId) -> Result<&V, StoreError> {
186        self.data.get(&id).ok_or_else(|| StoreError::not_found(id))
187    }
188
189    fn put_edge(&mut self, edge: V) -> Result<(), StoreError> {
190        let id = edge.edge_id();
191        self.data.insert(id, edge);
192        Ok(())
193    }
194
195    fn delete_edge(&mut self, id: EdgeId) -> Result<(), StoreError> {
196        self.data.remove(&id).ok_or_else(|| StoreError::not_found(id))?;
197        Ok(())
198    }
199
200    fn query_edges(&self, query: &Query) -> Result<QueryResult<&V>, StoreError> {
201        let all: Vec<&V> = self.data.values().collect();
202        let total = all.len();
203        Ok(paginate(all.into_iter(), total, query))
204    }
205}
206
207// ---------------------------------------------------------------------------
208// Tests
209// ---------------------------------------------------------------------------
210
211#[cfg(test)]
212mod tests {
213    use super::*;
214
215    // Minimal test types --------------------------------------------------
216
217    #[derive(Debug, Clone, PartialEq)]
218    struct TestAtom {
219        id: AtomId,
220        name: &'static str,
221    }
222    impl HasAtomId for TestAtom {
223        fn atom_id(&self) -> AtomId { self.id }
224    }
225
226    #[derive(Debug, Clone, PartialEq)]
227    struct TestBlock {
228        id: BlockId,
229    }
230    impl HasBlockId for TestBlock {
231        fn block_id(&self) -> BlockId { self.id }
232    }
233
234    #[derive(Debug, Clone, PartialEq)]
235    struct TestEdge {
236        id: EdgeId,
237    }
238    impl HasEdgeId for TestEdge {
239        fn edge_id(&self) -> EdgeId { self.id }
240    }
241
242    // AtomStore tests -------------------------------------------------------
243
244    #[test]
245    fn atom_put_and_get() {
246        let mut store = InMemoryAtomStore::new();
247        let id = AtomId::new(1);
248        store.put_atom(TestAtom { id, name: "a" }).unwrap();
249        assert_eq!(store.get_atom(id).unwrap().name, "a");
250    }
251
252    #[test]
253    fn atom_get_missing_returns_not_found() {
254        let store: InMemoryAtomStore<TestAtom> = InMemoryAtomStore::new();
255        let err = store.get_atom(AtomId::new(99)).unwrap_err();
256        assert!(matches!(err, StoreError::NotFound { .. }));
257    }
258
259    #[test]
260    fn atom_delete_removes_entry() {
261        let mut store = InMemoryAtomStore::new();
262        let id = AtomId::new(1);
263        store.put_atom(TestAtom { id, name: "x" }).unwrap();
264        store.delete_atom(id).unwrap();
265        assert!(store.get_atom(id).is_err());
266    }
267
268    #[test]
269    fn atom_delete_missing_returns_not_found() {
270        let mut store: InMemoryAtomStore<TestAtom> = InMemoryAtomStore::new();
271        assert!(matches!(
272            store.delete_atom(AtomId::new(5)).unwrap_err(),
273            StoreError::NotFound { .. }
274        ));
275    }
276
277    #[test]
278    fn atom_query_pagination() {
279        let mut store = InMemoryAtomStore::new();
280        for i in 0..5u64 {
281            store.put_atom(TestAtom { id: AtomId::new(i), name: "x" }).unwrap();
282        }
283        let q = Query::new().limit(2).offset(1);
284        let result = store.query_atoms(&q).unwrap();
285        assert_eq!(result.total, 5);
286        assert_eq!(result.items.len(), 2);
287        assert_eq!(result.offset, 1);
288    }
289
290    // BlockStore tests ------------------------------------------------------
291
292    #[test]
293    fn block_put_get_delete() {
294        let mut store = InMemoryBlockStore::new();
295        let id = BlockId::new(10);
296        store.put_block(TestBlock { id }).unwrap();
297        assert_eq!(store.get_block(id).unwrap().id, id);
298        store.delete_block(id).unwrap();
299        assert!(store.get_block(id).is_err());
300    }
301
302    // EdgeStore tests -------------------------------------------------------
303
304    #[test]
305    fn edge_put_get_delete() {
306        let mut store = InMemoryEdgeStore::new();
307        let id = EdgeId::new(20);
308        store.put_edge(TestEdge { id }).unwrap();
309        assert_eq!(store.get_edge(id).unwrap().id, id);
310        store.delete_edge(id).unwrap();
311        assert!(store.get_edge(id).is_err());
312    }
313}