use super::{Adapter, FindAllOptions, FindOptions, QueryRecordOptions};
use anyhow::{Result, anyhow};
use jsonapi_core::{Resource, ResourceObject};
use pluralizer::pluralize;
use serde_json::{Value, json};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Default)]
pub struct Store {
adapters: HashMap<String, Arc<dyn Adapter + Send + Sync>>,
cache: HashMap<String, HashMap<String, Resource>>,
}
impl Store {
pub fn new(adapters: HashMap<String, Arc<dyn Adapter + Send + Sync>>) -> Self {
Self {
adapters,
cache: HashMap::new(),
}
}
pub fn adapter_for(&self, model_name: &str) -> Result<Arc<dyn Adapter>> {
if let Some(adapter) = self.adapters.get(model_name) {
return Ok(adapter.clone());
}
const APPLICATION: &str = "application";
if model_name != APPLICATION
&& let Some(adapter) = self.adapters.get(APPLICATION)
{
return Ok(adapter.clone());
}
Err(anyhow!("No such adapter"))
}
pub async fn create_record<M>(&self, model: &M) -> Result<M>
where
M: ResourceObject,
{
let model_name = pluralize(model.resource_type(), 1, false);
let adapter = self.adapter_for(&model_name)?;
let resource: Resource = serde_json::from_value(serde_json::to_value(model)?)?;
let finalized_resource = adapter.create_record(resource).await?;
let json = serde_json::to_string(&finalized_resource)?;
let model: M = serde_json::from_str(&json)?;
Ok(model)
}
pub async fn delete_record<M>(&self, model: &M) -> Result<()>
where
M: ResourceObject,
{
let model_name = pluralize(model.resource_type(), 1, false);
let adapter = self.adapter_for(&model_name)?;
let resource: Resource = serde_json::from_value(serde_json::to_value(model)?)?;
adapter.delete_record(&resource).await?;
Ok(())
}
pub async fn find_all<M>(&self, resource_type: &str, _options: Option<FindAllOptions>) -> Result<Vec<M>>
where
M: ResourceObject,
{
assert_eq!(resource_type, pluralize(resource_type, 1, false));
let adapter = self.adapter_for(resource_type)?;
let collection = pluralize(resource_type, 2, false);
let query = json!({});
let resources = adapter.query(&collection, query).await?;
let json = serde_json::to_string(&resources)?;
let models: Vec<M> = serde_json::from_str(&json)?;
Ok(models)
}
pub async fn find_record<M>(&self, resource_type: &str, id: &str, _options: Option<FindOptions>) -> Result<M>
where
M: ResourceObject,
{
assert_eq!(resource_type, pluralize(resource_type, 1, false));
let adapter = self.adapter_for(resource_type)?;
let collection = pluralize(resource_type, 2, false);
let resource = adapter.find_record(&collection, id).await?;
let json = serde_json::to_string(&resource)?;
let model: M = serde_json::from_str(&json)?;
Ok(model)
}
pub fn peek_all<M>(&self, resource_type: &str) -> Result<Vec<M>>
where
M: ResourceObject,
{
assert_eq!(resource_type, pluralize(resource_type, 1, false));
let collection = pluralize(resource_type, 2, false);
match self.cache.get(&collection) {
None => Ok(vec![]),
Some(resources) => {
let mut v = Vec::new();
for resource in resources.values() {
let json = serde_json::to_string(resource)?;
let model: M = serde_json::from_str(&json)?;
v.push(model);
}
Ok(v)
}
}
}
pub fn peek_record<M>(&self, resource_type: &str, id: &str) -> Result<Option<M>>
where
M: ResourceObject,
{
assert_eq!(resource_type, pluralize(resource_type, 1, false));
let collection = pluralize(resource_type, 2, false);
match self.cache.get(&collection) {
None => Ok(None),
Some(resources) => match resources.get(id) {
None => Ok(None),
Some(resource) => {
let json = serde_json::to_string(resource)?;
let model: M = serde_json::from_str(&json)?;
Ok(Some(model))
}
},
}
}
pub fn push<M>(&mut self, model: M) -> Result<()>
where
M: ResourceObject,
{
let collection = model.resource_type();
let id = model.resource_id().unwrap_or_default().to_string();
let resource: Resource = serde_json::from_value(serde_json::to_value(&model)?)?;
match self.cache.get_mut(collection) {
None => {
self.cache
.insert(collection.to_string(), HashMap::from([(id, resource)]));
}
Some(map) => {
map.insert(id, resource);
}
}
Ok(())
}
pub async fn query<M>(&self, resource_type: &str, query: Value) -> Result<Vec<M>>
where
M: ResourceObject,
{
assert_eq!(resource_type, pluralize(resource_type, 1, false));
let adapter = self.adapter_for(resource_type)?;
let resources = adapter.query(resource_type, query).await?;
let mut models = Vec::with_capacity(resources.len());
for resource in resources {
let json = serde_json::to_string(&resource)?;
let model: M = serde_json::from_str(&json)?;
models.push(model);
}
Ok(models)
}
pub async fn query_record<M>(
&self,
resource_type: &str,
query: Value,
options: Option<QueryRecordOptions>,
) -> Result<Option<M>>
where
M: ResourceObject,
{
assert_eq!(resource_type, pluralize(resource_type, 1, false));
let adapter = self.adapter_for(resource_type)?;
let resource = adapter
.query_record(resource_type, query, options.unwrap_or_default())
.await?;
let model: Option<M> = match resource {
None => None,
Some(resource) => {
let json = serde_json::to_string(&resource)?;
serde_json::from_str(&json)?
}
};
Ok(model)
}
pub fn unload_all(&mut self, resource_type: &str) -> Result<()> {
assert_eq!(resource_type, pluralize(resource_type, 1, false));
let collection = pluralize(resource_type, 2, false);
if let Some(map) = self.cache.get_mut(&collection) {
map.clear();
}
Ok(())
}
pub fn unload_record<M>(&mut self, model: &M) -> Result<()>
where
M: ResourceObject,
{
let collection = model.resource_type();
if let Some(map) = self.cache.get_mut(collection) {
let id = model.resource_id().unwrap_or_default().to_string();
map.remove(id.as_str());
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::Store;
use crate::Config;
use crate::data::adapters::RestAdapter;
use crate::data::{Adapter, QueryRecordOptions};
use async_trait::async_trait;
use httptest::matchers::request;
use httptest::responders::{json_encoded, status_code};
use httptest::{Expectation, Server};
use jsonapi_core::{JsonApi, Resource};
use serde_json::{Value, json};
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Clone, Debug, JsonApi, PartialEq)]
#[jsonapi(type = "customers")]
struct Customer {
#[jsonapi(id)]
id: String,
}
#[tokio::test]
async fn cannot_peek_non_existing() {
let store = Store::default();
let maybe_customer = store
.peek_record::<Customer>("customer", "123")
.expect("Expected to peek customer record");
assert_eq!(maybe_customer, None);
}
#[tokio::test]
async fn can_push_then_peek_record() {
let mut store = Store::default();
let customer = Customer { id: "123".to_string() };
store.push(customer.clone()).expect("Expected to push customer record");
let maybe_customer = store
.peek_record::<Customer>("customer", "123")
.expect("Expected to peek customer record");
assert_eq!(maybe_customer, Some(customer));
}
#[tokio::test]
async fn can_push_then_peek_all() {
let mut store = Store::default();
let customer_1 = Customer { id: "123".to_string() };
store
.push(customer_1.clone())
.expect("Expected to push customer record");
let customer_2 = Customer { id: "456".to_string() };
store
.push(customer_2.clone())
.expect("Expected to push customer record");
let customers = store
.peek_all::<Customer>("customer")
.expect("Expected to peek at customer records");
assert_eq!(customers.len(), 2);
}
#[tokio::test]
async fn can_push_then_unload_all() {
let mut store = Store::default();
let customer = Customer { id: "123".to_string() };
store.push(customer.clone()).expect("Expected to push customer record");
assert!(
store
.peek_record::<Customer>("customer", "123")
.expect("Expected to peek customer record")
.is_some()
);
store
.unload_all("customer")
.expect("Expected to unload all customer records");
assert!(
store
.peek_record::<Customer>("customer", "123")
.expect("Expected to peek customer record")
.is_none()
);
}
#[tokio::test]
async fn can_push_then_unload_record() {
let mut store = Store::default();
let customer_1 = Customer { id: "123".to_string() };
let customer_2 = Customer { id: "456".to_string() };
store
.push(customer_1.clone())
.expect("Expected to push customer record");
store
.push(customer_2.clone())
.expect("Expected to push customer record");
assert!(
store
.peek_record::<Customer>("customer", "123")
.expect("Expected to peek customer record")
.is_some()
);
assert!(
store
.peek_record::<Customer>("customer", "456")
.expect("Expected to peek customer record")
.is_some()
);
store
.unload_record(&customer_2)
.expect("Expected to unload customer record");
assert!(
store
.peek_record::<Customer>("customer", "123")
.expect("Expected to peek customer record")
.is_some()
);
assert!(
store
.peek_record::<Customer>("customer", "456")
.expect("Expected to peek customer record")
.is_none()
);
}
#[tokio::test]
async fn cannot_find_non_existing_adapter() {
let store = Store::default();
let maybe_adapter = store.adapter_for("foo");
assert!(maybe_adapter.is_err());
}
#[tokio::test]
async fn can_find_adapter() {
#[derive(Default)]
struct FooAdapter {}
#[async_trait(?Send)]
impl Adapter for FooAdapter {
fn init(&mut self, _config: &Config) -> anyhow::Result<()> {
Ok(())
}
async fn create_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
todo!()
}
async fn delete_record(&self, _resource: &Resource) -> anyhow::Result<()> {
todo!()
}
async fn find_record(&self, _resource_type: &str, _id: &str) -> anyhow::Result<Resource> {
todo!()
}
async fn query(&self, _resource_type: &str, _query: Value) -> anyhow::Result<Vec<Resource>> {
todo!()
}
async fn query_record(
&self,
_resource_type: &str,
_query: Value,
_options: QueryRecordOptions,
) -> anyhow::Result<Option<Resource>> {
todo!()
}
async fn update_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
todo!()
}
}
const MODEL: &str = "foo";
let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(FooAdapter::default());
let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
let store = Store::new(adapters);
let maybe_adapter = store.adapter_for(MODEL);
assert!(maybe_adapter.is_ok());
}
#[tokio::test]
async fn can_default_to_application_adapter() {
#[derive(Default)]
struct ApplicationAdapter {}
#[async_trait(?Send)]
impl Adapter for ApplicationAdapter {
fn init(&mut self, _config: &Config) -> anyhow::Result<()> {
Ok(())
}
async fn create_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
todo!()
}
async fn delete_record(&self, _resource: &Resource) -> anyhow::Result<()> {
todo!()
}
async fn find_record(&self, _resource_type: &str, _id: &str) -> anyhow::Result<Resource> {
todo!()
}
async fn query(&self, _resource_type: &str, _query: Value) -> anyhow::Result<Vec<Resource>> {
todo!()
}
async fn query_record(
&self,
_resource_type: &str,
_query: Value,
_options: QueryRecordOptions,
) -> anyhow::Result<Option<Resource>> {
todo!()
}
async fn update_record(&self, _resource: Resource) -> anyhow::Result<Resource> {
todo!()
}
}
const MODEL: &str = "application";
let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(ApplicationAdapter::default());
let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
let store = Store::new(adapters);
let maybe_adapter = store.adapter_for("foo");
assert!(maybe_adapter.is_ok());
}
#[tokio::test]
async fn can_create_record() {
let server = Server::run();
server.expect(
Expectation::matching(request::method_path("POST", "/v1/customers")).respond_with(json_encoded(
json!({"customer": {"id": "123", "name": "Acme Widgets 2"}}),
)),
);
let server_url = server.url_str("/v1");
#[derive(Clone, JsonApi)]
#[jsonapi(type = "customers")]
struct Customer {
#[jsonapi(id)]
id: String,
name: String,
}
let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
const MODEL: &str = "customer";
let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
let store = Store::new(adapters);
let _ = store.adapter_for(MODEL);
let customer = Customer {
id: String::new(),
name: "Acme Widgets".to_string(),
};
let finalized_customer: Customer = store.create_record(&customer).await.expect("Expected to find customer");
assert_eq!(finalized_customer.id, "123".to_string());
assert_eq!(finalized_customer.name, "Acme Widgets 2".to_string());
}
#[tokio::test]
async fn can_delete_record() {
let server = Server::run();
server.expect(
Expectation::matching(request::method_path("DELETE", "/v1/customers/123")).respond_with(status_code(200)),
);
let server_url = server.url_str("/v1");
#[derive(Clone, JsonApi)]
#[jsonapi(type = "customers")]
struct Customer {
#[jsonapi(id)]
id: String,
name: String,
}
let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
const MODEL: &str = "customer";
let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
let store = Store::new(adapters);
let _ = store.adapter_for(MODEL);
let customer = Customer {
id: "123".to_string(),
name: "Acme Widgets".to_string(),
};
store.delete_record(&customer).await.expect("Epected to delete record");
}
#[tokio::test]
async fn can_find_all() {
let server = Server::run();
server.expect(
Expectation::matching(request::method_path("GET", "/v1/customers")).respond_with(json_encoded(
json!({"customers": [
{"id": "123", "name": "Acme Widgets"},
{"id": "456", "name": "Standard Paper Supplies, Inc."},
]}),
)),
);
let server_url = server.url_str("/v1");
#[derive(Debug, JsonApi)]
#[jsonapi(type = "customers")]
struct Customer {
#[jsonapi(id)]
id: String,
name: String,
}
let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
const MODEL: &str = "customer";
let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
let store = Store::new(adapters);
let _ = store.adapter_for(MODEL);
let customers: Vec<Customer> = store.find_all(MODEL, None).await.expect("Expected to find customers");
assert_eq!(customers.len(), 2);
}
#[tokio::test]
async fn can_find_record() {
let server = Server::run();
server.expect(
Expectation::matching(request::method_path("GET", "/v1/customers/123"))
.respond_with(json_encoded(json!({"customer": {"id": "123", "name": "Acme Widgets"}}))),
);
let server_url = server.url_str("/v1");
#[derive(Debug, JsonApi)]
#[jsonapi(type = "customers")]
struct Customer {
#[jsonapi(id)]
id: String,
name: String,
}
let adapter: Arc<dyn Adapter + Send + Sync> = Arc::new(RestAdapter::new(server_url));
const MODEL: &str = "customer";
let adapters = HashMap::from([(MODEL.to_string(), adapter)]);
let store = Store::new(adapters);
let _ = store.adapter_for(MODEL);
let customer: Customer = store
.find_record(MODEL, "123", None)
.await
.expect("Expected to find customer");
assert_eq!(customer.id, "123".to_string());
assert_eq!(customer.name, "Acme Widgets".to_string());
}
}