Skip to main content

cinderblock_core/data_layer/
in_memory.rs

1use std::{
2    any::{Any, TypeId},
3    collections::HashMap,
4    sync::{Arc, LazyLock},
5};
6
7use tokio::sync::RwLock;
8
9use crate::{PerformRead, ReadAction, Resource, data_layer::DataLayer};
10
11type State =
12    LazyLock<Arc<RwLock<HashMap<TypeId, HashMap<String, Box<dyn Any + Send + Sync + 'static>>>>>>;
13
14static STATE: State = LazyLock::new(Arc::default);
15
16#[derive(Debug)]
17pub struct InMemoryDataLayer {}
18impl InMemoryDataLayer {
19    pub(crate) fn new() -> Self {
20        Self {}
21    }
22
23    /// Load all rows of a given resource type from the global in-memory
24    /// store.
25    ///
26    /// This is used by generated relation-loading code to batch-fetch
27    /// related resources. Returns an empty `Vec` if no rows of the
28    /// requested type exist.
29    pub async fn load_all<R: Resource + 'static>(&self) -> Vec<R> {
30        let state = STATE.clone();
31        let state = state.read().await;
32
33        state
34            .get(&TypeId::of::<R>())
35            .into_iter()
36            .flat_map(|map| map.values())
37            .filter_map(|boxed| boxed.downcast_ref::<R>())
38            .cloned()
39            .collect()
40    }
41}
42
43impl<R: Resource + 'static> DataLayer<R> for InMemoryDataLayer {
44    async fn create(&self, resource: R) -> crate::Result<R> {
45        let state = STATE.clone();
46        let mut state = state.write().await;
47
48        let map = state.entry(TypeId::of::<R>()).or_default();
49        map.insert(
50            resource.primary_key().to_string(),
51            Box::new(resource.clone()),
52        );
53
54        Ok(resource)
55    }
56
57    async fn read(&self, primary_key: &R::PrimaryKey) -> crate::Result<R> {
58        let state = STATE.clone();
59        let state = state.read().await;
60
61        let key = primary_key.to_string();
62
63        state
64            .get(&TypeId::of::<R>())
65            .and_then(|map| map.get(&key))
66            .and_then(|boxed| boxed.downcast_ref::<R>())
67            .cloned()
68            .ok_or_else(|| format!("resource not found for primary key `{key}`").into())
69    }
70
71    async fn update(&self, resource: R) -> crate::Result<()> {
72        let state = STATE.clone();
73        let mut state = state.write().await;
74
75        let key = resource.primary_key().to_string();
76
77        let map = state
78            .get_mut(&TypeId::of::<R>())
79            .ok_or_else(|| format!("resource not found for primary key `{key}`"))?;
80
81        if !map.contains_key(&key) {
82            return Err(format!("resource not found for primary key `{key}`").into());
83        }
84
85        map.insert(key, Box::new(resource.clone()));
86
87        Ok(())
88    }
89
90    async fn destroy(&self, primary_key: &R::PrimaryKey) -> crate::Result<R> {
91        let state = STATE.clone();
92        let mut state = state.write().await;
93
94        let key = primary_key.to_string();
95
96        let map = state
97            .get_mut(&TypeId::of::<R>())
98            .ok_or_else(|| format!("resource not found for primary key `{key}`"))?;
99
100        let boxed = map
101            .remove(&key)
102            .ok_or_else(|| format!("resource not found for primary key `{key}`"))?;
103
104        boxed
105            .downcast::<R>()
106            .map(|r| *r)
107            .map_err(|_| "failed to downcast destroyed resource".into())
108    }
109}
110
111/// Filter trait for non-paged in-memory read actions. The generated
112/// `resource!` macro emits an impl for each non-paged read action.
113pub trait InMemoryReadAction: ReadAction {
114    fn filter(row: &Self::Output, args: &Self::Arguments) -> bool;
115}
116
117/// Filter trait for paged in-memory read actions. Same filter interface as
118/// [`InMemoryReadAction`], but applied to paged actions which additionally
119/// require `Arguments: Paged`.
120pub trait InMemoryPagedReadAction: ReadAction {
121    fn filter(row: &Self::Output, args: &Self::Arguments) -> bool;
122}
123
124/// Unified execution trait that bridges the filter-based traits
125/// ([`InMemoryReadAction`] and [`InMemoryPagedReadAction`]) to the
126/// framework's [`PerformRead`] trait.
127///
128/// The `resource!` macro generates explicit impls of this trait for each
129/// read action, delegating to the appropriate filter logic. A single
130/// blanket `PerformRead` impl then dispatches to this trait.
131pub trait InMemoryPerformRead: ReadAction {
132    fn execute(
133        all: impl Iterator<Item = Self::Output>,
134        args: &Self::Arguments,
135    ) -> Self::Response;
136}
137
138/// Single `PerformRead` impl for `InMemoryDataLayer` that delegates to
139/// `InMemoryPerformRead::execute`.
140impl<R, A> PerformRead<A> for InMemoryDataLayer
141where
142    R: Resource + 'static,
143    A: ReadAction<Output = R> + InMemoryPerformRead + 'static,
144{
145    async fn read(&self, args: &A::Arguments) -> crate::Result<A::Response> {
146        let state = STATE.clone();
147        let state = state.read().await;
148
149        let type_map = state.get(&TypeId::of::<R>());
150        let all = type_map
151            .iter()
152            .flat_map(|map| map.values())
153            .filter_map(|boxed| boxed.downcast_ref::<R>())
154            .cloned();
155
156        Ok(A::execute(all, args))
157    }
158}