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