Skip to main content

auric_runtime/data/
store.rs

1use super::{Adapter, FindAllOptions, FindOptions, QueryRecordOptions};
2use anyhow::{Result, anyhow};
3use jsonapi_core::{Resource, ResourceObject};
4use pluralizer::pluralize;
5use serde_json::Value;
6use std::collections::HashMap;
7use std::sync::Arc;
8
9#[derive(Default)]
10pub struct Store {
11    adapters: HashMap<String, Arc<dyn Adapter + Send + Sync>>,
12    cache: HashMap<String, HashMap<String, Resource>>,
13}
14
15impl Store {
16    pub fn new(adapters: HashMap<String, Arc<dyn Adapter + Send + Sync>>) -> Self {
17        Self {
18            adapters,
19            cache: HashMap::new(),
20        }
21    }
22
23    /// Returns an instance of the adapter for the given model name
24    pub fn adapter_for(&self, model_name: &str) -> Result<Arc<dyn Adapter>> {
25        // Find an adapter for the model name
26        if let Some(adapter) = self.adapters.get(model_name) {
27            return Ok(adapter.clone());
28        }
29
30        // Default to an application adapter
31        const APPLICATION: &str = "application";
32        if model_name != APPLICATION
33            && let Some(adapter) = self.adapters.get(APPLICATION)
34        {
35            return Ok(adapter.clone());
36        }
37
38        Err(anyhow!("No such adapter"))
39    }
40
41    /// Create a new record in the current store
42    pub async fn create_record<M>(&self, model: M) -> Result<M>
43    where
44        M: ResourceObject,
45    {
46        let model_name = pluralize(model.resource_type(), 1, false);
47        let adapter = self.adapter_for(&model_name)?;
48        let attributes: Value = serde_json::to_value(&model)?;
49        let resource = Resource {
50            type_: model.resource_type().to_string(),
51            id: model.resource_id().map(|id| id.to_string()),
52            lid: None,
53            attributes,
54            relationships: Default::default(),
55            links: None,
56            meta: None,
57        };
58        let finalized_resource = adapter.create_record(resource).await?;
59        let json = serde_json::to_string(&finalized_resource)?;
60        let model: M = serde_json::from_str(&json)?;
61        Ok(model)
62    }
63
64    /// Delete a record
65    pub async fn delete_record<M>(&self, model: M) -> Result<()>
66    where
67        M: ResourceObject,
68    {
69        let model_name = pluralize(model.resource_type(), 1, false);
70        let adapter = self.adapter_for(&model_name)?;
71        let attributes: Value = serde_json::to_value(&model)?;
72        let resource = Resource {
73            type_: model.resource_type().to_string(),
74            id: model.resource_id().map(|id| id.to_string()),
75            lid: None,
76            attributes,
77            relationships: Default::default(),
78            links: None,
79            meta: None,
80        };
81        adapter.delete_record(&resource).await?;
82        Ok(())
83    }
84
85    /// Find all the records for the given resource type
86    pub async fn find_all<M>(&self, resource_type: &str, _options: Option<FindAllOptions>) -> Result<Vec<M>>
87    where
88        M: ResourceObject,
89    {
90        assert_eq!(resource_type, pluralize(resource_type, 1, false));
91        let _adapter = self.adapter_for(resource_type)?;
92        todo!("NIY")
93    }
94
95    /// Find a single record
96    pub async fn find_record<M>(&self, resource_type: &str, id: &str, _options: Option<FindOptions>) -> Result<M>
97    where
98        M: ResourceObject,
99    {
100        assert_eq!(resource_type, pluralize(resource_type, 1, false));
101        let adapter = self.adapter_for(resource_type)?;
102        let collection = pluralize(resource_type, 2, false);
103        let resource = adapter.find_record(&collection, id).await?;
104        let json = serde_json::to_string(&resource)?;
105        let model: M = serde_json::from_str(&json)?;
106        Ok(model)
107    }
108
109    /// Check for a single record without triggering a fetch
110    pub fn peek_record<M>(&self, resource_type: &str, id: &str) -> Result<Option<M>>
111    where
112        M: ResourceObject,
113    {
114        assert_eq!(resource_type, pluralize(resource_type, 1, false));
115        let collection = pluralize(resource_type, 2, false);
116        match self.cache.get(&collection) {
117            None => Ok(None),
118            Some(resources) => match resources.get(id) {
119                None => Ok(None),
120                Some(resource) => {
121                    let json = serde_json::to_string(resource)?;
122                    let model: M = serde_json::from_str(&json)?;
123                    Ok(Some(model))
124                }
125            },
126        }
127    }
128
129    /// Push a model into the store
130    pub fn push<M>(&mut self, model: M) -> Result<()>
131    where
132        M: ResourceObject,
133    {
134        let collection = model.resource_type();
135        let id = model.resource_id().unwrap_or_default().to_string();
136        let json = serde_json::to_string(&model)?;
137        let resource: Resource = serde_json::from_str(&json)?;
138        match self.cache.get_mut(collection) {
139            None => {
140                self.cache
141                    .insert(collection.to_string(), HashMap::from([(id, resource)]));
142            }
143            Some(map) => {
144                map.insert(id, resource);
145            }
146        }
147        Ok(())
148    }
149
150    /// Delegate a query to the adapter
151    pub async fn query<M>(&self, resource_type: &str, _query: serde_json::Value) -> Result<Vec<M>>
152    where
153        M: ResourceObject,
154    {
155        assert_eq!(resource_type, pluralize(resource_type, 1, false));
156        let _adapter = self.adapter_for(resource_type)?;
157        todo!("NIY")
158    }
159
160    /// Delegate the request for a single record to the adapter
161    pub async fn query_record<M>(
162        &self,
163        resource_type: &str,
164        _query: serde_json::Value,
165        _options: Option<QueryRecordOptions>,
166    ) -> Result<Vec<M>>
167    where
168        M: ResourceObject,
169    {
170        assert_eq!(resource_type, pluralize(resource_type, 1, false));
171        let _adapter = self.adapter_for(resource_type)?;
172        todo!("NIY")
173    }
174
175    /// Unloads all records in the store for the given resource type
176    pub fn unload_all(&mut self, resource_type: &str) -> Result<()> {
177        assert_eq!(resource_type, pluralize(resource_type, 1, false));
178        let collection = pluralize(resource_type, 2, false);
179        if let Some(map) = self.cache.get_mut(&collection) {
180            map.clear();
181        }
182        Ok(())
183    }
184
185    /// Unload a single record from the store
186    pub fn unload_record<M>(&mut self, model: &M) -> Result<()>
187    where
188        M: ResourceObject,
189    {
190        let collection = model.resource_type();
191        if let Some(map) = self.cache.get_mut(collection) {
192            let id = model.resource_id().unwrap_or_default().to_string();
193            map.remove(id.as_str());
194        }
195        Ok(())
196    }
197}
198
199#[cfg(test)]
200mod tests {
201    use super::Store;
202    use crate::data::adapters::RestAdapter;
203    use crate::data::{Adapter, QueryRecordOptions};
204    use async_trait::async_trait;
205    use httptest::matchers::request;
206    use httptest::responders::{json_encoded, status_code};
207    use httptest::{Expectation, Server};
208    use jsonapi_core::{JsonApi, Resource};
209    use serde_json::{Value, json};
210    use std::collections::HashMap;
211    use std::sync::Arc;
212
213    #[derive(Clone, Debug, JsonApi, PartialEq)]
214    #[jsonapi(type = "customers")]
215    struct Customer {
216        #[jsonapi(id)]
217        id: String,
218    }
219
220    #[tokio::test]
221    async fn cannot_peek_non_existing() {
222        let store = Store::default();
223        let maybe_customer = store.peek_record::<Customer>("customer", "123").unwrap();
224        assert_eq!(maybe_customer, None);
225    }
226
227    #[tokio::test]
228    async fn can_push_then_peek_record() {
229        let mut store = Store::default();
230        let customer = Customer { id: "123".to_string() };
231        store.push(customer.clone()).unwrap();
232        let maybe_customer = store.peek_record::<Customer>("customer", "123").unwrap();
233        assert_eq!(maybe_customer, Some(customer));
234    }
235
236    #[tokio::test]
237    async fn can_push_then_unload_all() {
238        let mut store = Store::default();
239        let customer = Customer { id: "123".to_string() };
240        store.push(customer.clone()).unwrap();
241        assert!(store.peek_record::<Customer>("customer", "123").unwrap().is_some());
242        store.unload_all("customer").unwrap();
243        assert!(store.peek_record::<Customer>("customer", "123").unwrap().is_none());
244    }
245
246    #[tokio::test]
247    async fn can_push_then_unload_record() {
248        let mut store = Store::default();
249        let customer_1 = Customer { id: "123".to_string() };
250        let customer_2 = Customer { id: "456".to_string() };
251        store.push(customer_1.clone()).unwrap();
252        store.push(customer_2.clone()).unwrap();
253        assert!(store.peek_record::<Customer>("customer", "123").unwrap().is_some());
254        assert!(store.peek_record::<Customer>("customer", "456").unwrap().is_some());
255        store.unload_record(&customer_2).unwrap();
256        assert!(store.peek_record::<Customer>("customer", "123").unwrap().is_some());
257        assert!(store.peek_record::<Customer>("customer", "456").unwrap().is_none());
258    }
259
260    #[tokio::test]
261    async fn cannot_find_non_existing_adapter() {
262        let store = Store::default();
263        let maybe_adapter = store.adapter_for("foo");
264        assert!(maybe_adapter.is_err());
265    }
266
267    #[tokio::test]
268    async fn can_find_adapter() {
269        #[derive(Default)]
270        struct FooAdapter {}
271        #[async_trait(?Send)]
272        impl Adapter for FooAdapter {
273            async fn create_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
274                todo!()
275            }
276            async fn delete_record(&self, _resource: &Resource) -> anyhow::Result<()> {
277                todo!()
278            }
279            async fn find_record(&self, _resource_type: &str, _id: &str) -> anyhow::Result<Resource> {
280                todo!()
281            }
282            async fn query(&self, _resource_type: &str, _query: Value) -> anyhow::Result<Vec<Resource>> {
283                todo!()
284            }
285            async fn query_record(
286                &self,
287                _resource_type: &str,
288                _query: Value,
289                _options: QueryRecordOptions,
290            ) -> anyhow::Result<Resource> {
291                todo!()
292            }
293            async fn update_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
294                todo!()
295            }
296        }
297        const MODEL: &str = "foo";
298        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(FooAdapter::default());
299        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
300        let store = Store::new(adapters);
301        let maybe_adapter = store.adapter_for(MODEL);
302        assert!(maybe_adapter.is_ok());
303    }
304
305    #[tokio::test]
306    async fn can_default_to_application_adapter() {
307        #[derive(Default)]
308        struct ApplicationAdapter {}
309        #[async_trait(?Send)]
310        impl Adapter for ApplicationAdapter {
311            async fn create_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
312                todo!()
313            }
314            async fn delete_record(&self, _resource: &Resource) -> anyhow::Result<()> {
315                todo!()
316            }
317            async fn find_record(&self, _resource_type: &str, _id: &str) -> anyhow::Result<Resource> {
318                todo!()
319            }
320            async fn query(&self, _resource_type: &str, _query: Value) -> anyhow::Result<Vec<Resource>> {
321                todo!()
322            }
323            async fn query_record(
324                &self,
325                _resource_type: &str,
326                _query: Value,
327                _options: QueryRecordOptions,
328            ) -> anyhow::Result<Resource> {
329                todo!()
330            }
331            async fn update_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
332                todo!()
333            }
334        }
335        const MODEL: &str = "application";
336        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(ApplicationAdapter::default());
337        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
338        let store = Store::new(adapters);
339        let maybe_adapter = store.adapter_for("foo");
340        assert!(maybe_adapter.is_ok());
341    }
342
343    #[tokio::test]
344    async fn can_create_record() {
345        // Setup mock server and expections for a find Customer operation
346        let server = Server::run();
347        server.expect(
348            Expectation::matching(request::method_path("POST", "/v1/customers")).respond_with(json_encoded(
349                json!({"customer": {"id": "123", "name": "Acme Widgets 2"}}),
350            )),
351        );
352        let server_url = server.url_str("/v1");
353
354        #[derive(Clone, JsonApi)]
355        #[jsonapi(type = "customers")]
356        struct Customer {
357            #[jsonapi(id)]
358            id: String,
359            name: String,
360        }
361
362        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
363        const MODEL: &str = "customer";
364        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
365        let store = Store::new(adapters);
366        let _ = store.adapter_for(MODEL);
367        let customer = Customer {
368            id: String::new(),
369            name: "Acme Widgets".to_string(),
370        };
371        let finalized_customer: Customer = store.create_record(customer.clone()).await.unwrap();
372        assert_eq!(finalized_customer.id, "123".to_string());
373        assert_eq!(finalized_customer.name, "Acme Widgets 2".to_string());
374    }
375
376    #[tokio::test]
377    async fn can_delete_record() {
378        // Setup mock server and expections for a find Customer operation
379        let server = Server::run();
380        server.expect(
381            Expectation::matching(request::method_path("DELETE", "/v1/customers/123")).respond_with(status_code(200)),
382        );
383        let server_url = server.url_str("/v1");
384
385        #[derive(Clone, JsonApi)]
386        #[jsonapi(type = "customers")]
387        struct Customer {
388            #[jsonapi(id)]
389            id: String,
390            name: String,
391        }
392
393        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
394        const MODEL: &str = "customer";
395        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
396        let store = Store::new(adapters);
397        let _ = store.adapter_for(MODEL);
398        let customer = Customer {
399            id: "123".to_string(),
400            name: "Acme Widgets".to_string(),
401        };
402        store.delete_record(customer).await.unwrap();
403    }
404
405    #[tokio::test]
406    async fn can_find_record() {
407        // Setup mock server and expections for a find Customer operation
408        let server = Server::run();
409        server.expect(
410            Expectation::matching(request::method_path("GET", "/v1/customers/123"))
411                .respond_with(json_encoded(json!({"customer": {"id": "123", "name": "Acme Widgets"}}))),
412        );
413        let server_url = server.url_str("/v1");
414
415        #[derive(Debug, JsonApi)]
416        #[jsonapi(type = "customers")]
417        struct Customer {
418            #[jsonapi(id)]
419            id: String,
420            name: String,
421        }
422
423        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
424        const MODEL: &str = "customer";
425        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
426        let store = Store::new(adapters);
427        let _ = store.adapter_for(MODEL);
428        let customer: Customer = store.find_record(MODEL, "123", None).await.unwrap();
429        assert_eq!(customer.id, "123".to_string());
430        assert_eq!(customer.name, "Acme Widgets".to_string());
431    }
432}