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, json};
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<B: AsRef<str>>(&self, bucket: B) -> Result<Arc<dyn Adapter + Send + Sync>> {
25        // Find an adapter for the model name
26        let model_name = pluralize(bucket.as_ref(), 1, false);
27        if let Some(adapter) = self.adapters.get(&model_name) {
28            return Ok(adapter.clone());
29        }
30
31        // Default to an application adapter
32        const APPLICATION: &str = "application";
33        if model_name != APPLICATION
34            && let Some(adapter) = self.adapters.get(APPLICATION)
35        {
36            return Ok(adapter.clone());
37        }
38
39        Err(anyhow!("No such adapter"))
40    }
41
42    /// Create a new record in the current store
43    pub async fn create_record<M>(&self, model: &M) -> Result<M>
44    where
45        M: ResourceObject,
46    {
47        let model_name = pluralize(model.resource_type(), 1, false);
48        let adapter = self.adapter_for(&model_name)?;
49        let resource = Resource::from_typed(model)?;
50        let finalized_resource = adapter.create_record(resource).await?;
51        let json = serde_json::to_string(&finalized_resource)?;
52        let model: M = serde_json::from_str(&json)?;
53        Ok(model)
54    }
55
56    /// Delete a record
57    pub async fn delete_record<M>(&self, model: &M) -> Result<()>
58    where
59        M: ResourceObject,
60    {
61        let model_name = pluralize(model.resource_type(), 1, false);
62        let adapter = self.adapter_for(&model_name)?;
63        let resource = Resource::from_typed(model)?;
64        adapter.delete_record(&resource).await?;
65        Ok(())
66    }
67
68    /// Find all the records for the given resource type
69    pub async fn find_all<B: AsRef<str>, M>(&self, bucket: B, _options: Option<FindAllOptions>) -> Result<Vec<M>>
70    where
71        M: ResourceObject,
72    {
73        let collection = bucket.as_ref();
74        let model_name = pluralize(collection, 1, false);
75        let adapter = self.adapter_for(&model_name)?;
76        let query = json!({});
77        let resources = adapter.query(collection, query).await?;
78        let json = serde_json::to_string(&resources)?;
79        let models: Vec<M> = serde_json::from_str(&json)?;
80        Ok(models)
81    }
82
83    /// Find a single record
84    pub async fn find_record<B: AsRef<str>, M>(&self, bucket: B, id: &str, _options: Option<FindOptions>) -> Result<M>
85    where
86        M: ResourceObject,
87    {
88        let collection = bucket.as_ref();
89        let model_name = pluralize(collection, 1, false);
90        let adapter = self.adapter_for(&model_name)?;
91        let resource = adapter.find_record(collection, id).await?;
92        let json = serde_json::to_string(&resource)?;
93        let model: M = serde_json::from_str(&json)?;
94        Ok(model)
95    }
96
97    /// Check for all records of a given resource type, without triggering a fetch
98    pub fn peek_all<B: AsRef<str>, M>(&self, bucket: B) -> Result<Vec<M>>
99    where
100        M: ResourceObject,
101    {
102        let collection = bucket.as_ref();
103        match self.cache.get(collection) {
104            None => Ok(vec![]),
105            Some(resources) => {
106                let mut v = Vec::new();
107                for resource in resources.values() {
108                    let json = serde_json::to_string(resource)?;
109                    let model: M = serde_json::from_str(&json)?;
110                    v.push(model);
111                }
112                Ok(v)
113            }
114        }
115    }
116
117    /// Check for a single record without triggering a fetch
118    pub fn peek_record<B: AsRef<str>, M>(&self, bucket: B, id: &str) -> Result<Option<M>>
119    where
120        M: ResourceObject,
121    {
122        let collection = bucket.as_ref();
123        match self.cache.get(collection) {
124            None => Ok(None),
125            Some(resources) => match resources.get(id) {
126                None => Ok(None),
127                Some(resource) => {
128                    let json = serde_json::to_string(resource)?;
129                    let model: M = serde_json::from_str(&json)?;
130                    Ok(Some(model))
131                }
132            },
133        }
134    }
135
136    /// Push a model into the store
137    pub fn push<M>(&mut self, model: M) -> Result<()>
138    where
139        M: ResourceObject,
140    {
141        let collection = model.resource_type();
142        let id = model.resource_id().unwrap_or_default().to_string();
143        let resource = Resource::from_typed(&model)?;
144        match self.cache.get_mut(collection) {
145            None => {
146                self.cache
147                    .insert(collection.to_string(), HashMap::from([(id, resource)]));
148            }
149            Some(map) => {
150                map.insert(id, resource);
151            }
152        }
153        Ok(())
154    }
155
156    /// Delegate a query to the adapter
157    pub async fn query<B: AsRef<str>, M>(&self, bucket: B, query: Value) -> Result<Vec<M>>
158    where
159        M: ResourceObject,
160    {
161        let collection = bucket.as_ref();
162        let model_name = pluralize(collection, 1, false);
163        let adapter = self.adapter_for(&model_name)?;
164        let resources = adapter.query(collection, query).await?;
165        let mut models = Vec::with_capacity(resources.len());
166        for resource in resources {
167            let json = serde_json::to_string(&resource)?;
168            let model: M = serde_json::from_str(&json)?;
169            models.push(model);
170        }
171        Ok(models)
172    }
173
174    /// Delegate the request for a single record to the adapter
175    pub async fn query_record<B: AsRef<str>, M>(
176        &self,
177        bucket: B,
178        query: Value,
179        options: Option<QueryRecordOptions>,
180    ) -> Result<Option<M>>
181    where
182        M: ResourceObject,
183    {
184        let collection = bucket.as_ref();
185        let model_name = pluralize(collection, 1, false);
186        let adapter = self.adapter_for(&model_name)?;
187        let resource = adapter
188            .query_record(collection, query, options.unwrap_or_default())
189            .await?;
190        let model: Option<M> = match resource {
191            None => None,
192            Some(resource) => {
193                let json = serde_json::to_string(&resource)?;
194                serde_json::from_str(&json)?
195            }
196        };
197        Ok(model)
198    }
199
200    /// Unloads all records in the store for the given resource type
201    pub fn unload_all<B: AsRef<str>>(&mut self, bucket: B) -> Result<()> {
202        let collection = bucket.as_ref();
203        if let Some(map) = self.cache.get_mut(collection) {
204            map.clear();
205        }
206        Ok(())
207    }
208
209    /// Unload a single record from the store
210    pub fn unload_record<M>(&mut self, model: &M) -> Result<()>
211    where
212        M: ResourceObject,
213    {
214        let collection = model.resource_type();
215        if let Some(map) = self.cache.get_mut(collection) {
216            let id = model.resource_id().unwrap_or_default().to_string();
217            map.remove(id.as_str());
218        }
219        Ok(())
220    }
221}
222
223#[cfg(test)]
224mod tests {
225    use super::Store;
226    use crate::Config;
227    use crate::data::adapters::RestAdapter;
228    use crate::data::{Adapter, QueryRecordOptions};
229    use async_trait::async_trait;
230    use httptest::matchers::request;
231    use httptest::responders::{json_encoded, status_code};
232    use httptest::{Expectation, Server};
233    use jsonapi_core::{JsonApi, Resource};
234    use serde_json::{Value, json};
235    use std::collections::HashMap;
236    use std::sync::Arc;
237    use strum_macros::AsRefStr;
238
239    #[derive(AsRefStr, Debug)]
240    enum Bucket {
241        #[strum(serialize = "customers")]
242        Customers,
243    }
244
245    #[derive(Clone, Debug, JsonApi, PartialEq)]
246    #[jsonapi(type = "customers")]
247    struct Customer {
248        #[jsonapi(id)]
249        id: String,
250    }
251
252    #[tokio::test]
253    async fn cannot_peek_non_existing() {
254        let store = Store::default();
255        let maybe_customer = store
256            .peek_record::<_, Customer>(Bucket::Customers, "123")
257            .expect("Expected to peek customer record");
258        assert_eq!(maybe_customer, None);
259    }
260
261    #[tokio::test]
262    async fn can_push_then_peek_record() {
263        let mut store = Store::default();
264        let customer = Customer { id: "123".to_string() };
265        store.push(customer.clone()).expect("Expected to push customer record");
266        let maybe_customer = store
267            .peek_record::<_, Customer>(Bucket::Customers, "123")
268            .expect("Expected to peek customer record");
269        assert_eq!(maybe_customer, Some(customer));
270    }
271
272    #[tokio::test]
273    async fn can_push_then_peek_all() {
274        let mut store = Store::default();
275        let customer_1 = Customer { id: "123".to_string() };
276        store
277            .push(customer_1.clone())
278            .expect("Expected to push customer record");
279        let customer_2 = Customer { id: "456".to_string() };
280        store
281            .push(customer_2.clone())
282            .expect("Expected to push customer record");
283        let customers = store
284            .peek_all::<_, Customer>(Bucket::Customers)
285            .expect("Expected to peek at customer records");
286        assert_eq!(customers.len(), 2);
287    }
288
289    #[tokio::test]
290    async fn can_push_then_unload_all() {
291        let mut store = Store::default();
292        let customer = Customer { id: "123".to_string() };
293        store.push(customer.clone()).expect("Expected to push customer record");
294        assert!(
295            store
296                .peek_record::<_, Customer>(Bucket::Customers, "123")
297                .expect("Expected to peek customer record")
298                .is_some()
299        );
300        store
301            .unload_all(Bucket::Customers)
302            .expect("Expected to unload all customer records");
303        assert!(
304            store
305                .peek_record::<_, Customer>(Bucket::Customers, "123")
306                .expect("Expected to peek customer record")
307                .is_none()
308        );
309    }
310
311    #[tokio::test]
312    async fn can_push_then_unload_record() {
313        let mut store = Store::default();
314        let customer_1 = Customer { id: "123".to_string() };
315        let customer_2 = Customer { id: "456".to_string() };
316        store
317            .push(customer_1.clone())
318            .expect("Expected to push customer record");
319        store
320            .push(customer_2.clone())
321            .expect("Expected to push customer record");
322        assert!(
323            store
324                .peek_record::<_, Customer>(Bucket::Customers, "123")
325                .expect("Expected to peek customer record")
326                .is_some()
327        );
328        assert!(
329            store
330                .peek_record::<_, Customer>(Bucket::Customers, "456")
331                .expect("Expected to peek customer record")
332                .is_some()
333        );
334        store
335            .unload_record(&customer_2)
336            .expect("Expected to unload customer record");
337        assert!(
338            store
339                .peek_record::<_, Customer>(Bucket::Customers, "123")
340                .expect("Expected to peek customer record")
341                .is_some()
342        );
343        assert!(
344            store
345                .peek_record::<_, Customer>(Bucket::Customers, "456")
346                .expect("Expected to peek customer record")
347                .is_none()
348        );
349    }
350
351    #[tokio::test]
352    async fn cannot_find_non_existing_adapter() {
353        let store = Store::default();
354        let maybe_adapter = store.adapter_for("foo");
355        assert!(maybe_adapter.is_err());
356    }
357
358    #[tokio::test]
359    async fn can_find_adapter() {
360        #[derive(Default)]
361        struct FooAdapter {}
362        #[async_trait(?Send)]
363        impl Adapter for FooAdapter {
364            fn init(&mut self, _config: &Config) -> anyhow::Result<()> {
365                Ok(())
366            }
367            async fn create_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
368                todo!()
369            }
370            async fn delete_record(&self, _resource: &Resource) -> anyhow::Result<()> {
371                todo!()
372            }
373            async fn find_record(&self, _resource_type: &str, _id: &str) -> anyhow::Result<Resource> {
374                todo!()
375            }
376            async fn query(&self, _resource_type: &str, _query: Value) -> anyhow::Result<Vec<Resource>> {
377                todo!()
378            }
379            async fn query_record(
380                &self,
381                _resource_type: &str,
382                _query: Value,
383                _options: QueryRecordOptions,
384            ) -> anyhow::Result<Option<Resource>> {
385                todo!()
386            }
387            async fn update_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
388                todo!()
389            }
390        }
391        const MODEL: &str = "foo";
392        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(FooAdapter::default());
393        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
394        let store = Store::new(adapters);
395        let maybe_adapter = store.adapter_for(MODEL);
396        assert!(maybe_adapter.is_ok());
397    }
398
399    #[tokio::test]
400    async fn can_default_to_application_adapter() {
401        #[derive(Default)]
402        struct ApplicationAdapter {}
403        #[async_trait(?Send)]
404        impl Adapter for ApplicationAdapter {
405            fn init(&mut self, _config: &Config) -> anyhow::Result<()> {
406                Ok(())
407            }
408            async fn create_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
409                todo!()
410            }
411            async fn delete_record(&self, _resource: &Resource) -> anyhow::Result<()> {
412                todo!()
413            }
414            async fn find_record(&self, _resource_type: &str, _id: &str) -> anyhow::Result<Resource> {
415                todo!()
416            }
417            async fn query(&self, _resource_type: &str, _query: Value) -> anyhow::Result<Vec<Resource>> {
418                todo!()
419            }
420            async fn query_record(
421                &self,
422                _resource_type: &str,
423                _query: Value,
424                _options: QueryRecordOptions,
425            ) -> anyhow::Result<Option<Resource>> {
426                todo!()
427            }
428            async fn update_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
429                todo!()
430            }
431        }
432        const MODEL: &str = "application";
433        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(ApplicationAdapter::default());
434        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
435        let store = Store::new(adapters);
436        let maybe_adapter = store.adapter_for("foo");
437        assert!(maybe_adapter.is_ok());
438    }
439
440    #[tokio::test]
441    async fn can_create_record() {
442        // Setup mock server and expections for a find customer operation
443        let server = Server::run();
444        server.expect(
445            Expectation::matching(request::method_path("POST", "/v1/customers")).respond_with(json_encoded(
446                json!({"customer": {"id": "123", "name": "Acme Widgets 2"}}),
447            )),
448        );
449        let server_url = server.url_str("/v1");
450
451        #[derive(Clone, JsonApi)]
452        #[jsonapi(type = "customers")]
453        struct Customer {
454            #[jsonapi(id)]
455            id: String,
456            name: String,
457        }
458
459        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
460        const MODEL: &str = "customer";
461        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
462        let store = Store::new(adapters);
463        let _ = store.adapter_for(MODEL);
464        let customer = Customer {
465            id: String::new(),
466            name: "Acme Widgets".to_string(),
467        };
468        let finalized_customer: Customer = store.create_record(&customer).await.expect("Expected to find customer");
469        assert_eq!(finalized_customer.id, "123".to_string());
470        assert_eq!(finalized_customer.name, "Acme Widgets 2".to_string());
471    }
472
473    #[tokio::test]
474    async fn can_delete_record() {
475        // Setup mock server and expections for a find customer operation
476        let server = Server::run();
477        server.expect(
478            Expectation::matching(request::method_path("DELETE", "/v1/customers/123")).respond_with(status_code(200)),
479        );
480        let server_url = server.url_str("/v1");
481
482        #[derive(Clone, JsonApi)]
483        #[jsonapi(type = "customers")]
484        struct Customer {
485            #[jsonapi(id)]
486            id: String,
487            name: String,
488        }
489
490        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
491        const MODEL: &str = "customer";
492        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
493        let store = Store::new(adapters);
494        let _ = store.adapter_for(MODEL);
495        let customer = Customer {
496            id: "123".to_string(),
497            name: "Acme Widgets".to_string(),
498        };
499        store.delete_record(&customer).await.expect("Epected to delete record");
500    }
501
502    #[tokio::test]
503    async fn can_find_all() {
504        // Setup mock server and expections for a find customers operation
505        let server = Server::run();
506        server.expect(
507            Expectation::matching(request::method_path("GET", "/v1/customers")).respond_with(json_encoded(
508                json!({"customers": [
509                    {"id": "123", "name": "Acme Widgets"},
510                    {"id": "456", "name": "Standard Paper Supplies, Inc."},
511                ]}),
512            )),
513        );
514        let server_url = server.url_str("/v1");
515
516        #[derive(Debug, JsonApi)]
517        #[jsonapi(type = "customers")]
518        struct Customer {
519            #[jsonapi(id)]
520            id: String,
521            name: String,
522        }
523
524        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
525        const MODEL: &str = "customer";
526        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
527        let store = Store::new(adapters);
528        let _ = store.adapter_for(MODEL);
529        let customers: Vec<Customer> = store
530            .find_all(Bucket::Customers, None)
531            .await
532            .expect("Expected to find customers");
533        assert_eq!(customers.len(), 2);
534    }
535
536    #[tokio::test]
537    async fn can_find_record() {
538        // Setup mock server and expections for a find customer operation
539        let server = Server::run();
540        server.expect(
541            Expectation::matching(request::method_path("GET", "/v1/customers/123"))
542                .respond_with(json_encoded(json!({"customer": {"id": "123", "name": "Acme Widgets"}}))),
543        );
544        let server_url = server.url_str("/v1");
545
546        #[derive(Debug, JsonApi)]
547        #[jsonapi(type = "customers")]
548        struct Customer {
549            #[jsonapi(id)]
550            id: String,
551            name: String,
552        }
553
554        let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
555        const MODEL: &str = "customer";
556        let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
557        let store = Store::new(adapters);
558        let _ = store.adapter_for(MODEL);
559        let customer: Customer = store
560            .find_record(Bucket::Customers, "123", None)
561            .await
562            .expect("Expected to find customer");
563        assert_eq!(customer.id, "123".to_string());
564        assert_eq!(customer.name, "Acme Widgets".to_string());
565    }
566}