From 1a8db405dea97fabe44bd79f355826b4e20522e9 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Mon, 15 May 2023 18:18:26 +1000 Subject: [PATCH] Add an in-memory storage adaptor --- backend/Cargo.lock | 12 +++ backend/Cargo.toml | 2 + backend/adaptors/memory/Cargo.toml | 10 ++ backend/adaptors/memory/src/lib.rs | 146 +++++++++++++++++++++++++++++ backend/common/src/event.rs | 2 + backend/common/src/person.rs | 1 + backend/common/src/stats.rs | 1 + backend/src/adaptors.rs | 13 +++ backend/src/main.rs | 5 +- 9 files changed, 190 insertions(+), 2 deletions(-) create mode 100644 backend/adaptors/memory/Cargo.toml create mode 100644 backend/adaptors/memory/src/lib.rs create mode 100644 backend/src/adaptors.rs diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 03d0484..025bf58 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -616,7 +616,9 @@ dependencies = [ "bcrypt", "chrono", "common", + "datastore-adaptor", "dotenv", + "memory-adaptor", "punycode", "rand", "regex", @@ -1573,6 +1575,16 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +[[package]] +name = "memory-adaptor" +version = "0.1.0" +dependencies = [ + "async-trait", + "chrono", + "common", + "tokio", +] + [[package]] name = "mime" version = "0.3.17" diff --git a/backend/Cargo.toml b/backend/Cargo.toml index c943b8b..e0da7b4 100644 --- a/backend/Cargo.toml +++ b/backend/Cargo.toml @@ -14,6 +14,8 @@ serde = { version = "1.0.162", features = ["derive"] } tokio = { version = "1.28.0", features = ["macros", "rt-multi-thread"] } common = { path = "common" } sql-adaptor = { path = "adaptors/sql" } +datastore-adaptor = { path = "adaptors/datastore" } +memory-adaptor = { path = "adaptors/memory" } dotenv = "0.15.0" serde_json = "1.0.96" rand = "0.8.5" diff --git a/backend/adaptors/memory/Cargo.toml b/backend/adaptors/memory/Cargo.toml new file mode 100644 index 0000000..b5280ca --- /dev/null +++ b/backend/adaptors/memory/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "memory-adaptor" +version = "0.1.0" +edition = "2021" + +[dependencies] +async-trait = "0.1.68" +chrono = "0.4.24" +common = { path = "../../common" } +tokio = { version = "1.28.1", features = ["rt-multi-thread"] } diff --git a/backend/adaptors/memory/src/lib.rs b/backend/adaptors/memory/src/lib.rs new file mode 100644 index 0000000..200f8c4 --- /dev/null +++ b/backend/adaptors/memory/src/lib.rs @@ -0,0 +1,146 @@ +use std::{collections::HashMap, error::Error, fmt::Display}; + +use async_trait::async_trait; +use chrono::Utc; +use common::{ + adaptor::Adaptor, + event::{Event, EventDeletion}, + person::Person, + stats::Stats, +}; +use tokio::sync::Mutex; + +struct State { + stats: Stats, + events: HashMap, + people: HashMap<(String, String), Person>, +} + +pub struct MemoryAdaptor { + state: Mutex, +} + +#[async_trait] +impl Adaptor for MemoryAdaptor { + type Error = MemoryAdaptorError; + + async fn get_stats(&self) -> Result { + let state = self.state.lock().await; + + Ok(state.stats.clone()) + } + + async fn increment_stat_event_count(&self) -> Result { + let mut state = self.state.lock().await; + + state.stats.event_count += 1; + Ok(state.stats.event_count) + } + + async fn increment_stat_person_count(&self) -> Result { + let mut state = self.state.lock().await; + + state.stats.person_count += 1; + Ok(state.stats.person_count) + } + + async fn get_people(&self, event_id: String) -> Result>, Self::Error> { + let state = self.state.lock().await; + + // Event doesn't exist + if state.events.get(&event_id).is_none() { + return Ok(None); + } + + Ok(Some( + state + .people + .clone() + .into_iter() + .filter_map(|((p_event_id, _), p)| { + if p_event_id == event_id { + Some(p) + } else { + None + } + }) + .collect(), + )) + } + + async fn upsert_person(&self, event_id: String, person: Person) -> Result { + let mut state = self.state.lock().await; + + state + .people + .insert((event_id, person.name.clone()), person.clone()); + + Ok(person) + } + + async fn get_event(&self, id: String) -> Result, Self::Error> { + let mut state = self.state.lock().await; + + let event = state.events.get(&id).cloned(); + if let Some(mut event) = event.clone() { + event.visited_at = Utc::now(); + state.events.insert(id, event); + } + + Ok(event) + } + + async fn create_event(&self, event: Event) -> Result { + let mut state = self.state.lock().await; + + state.events.insert(event.id.clone(), event.clone()); + + Ok(event) + } + + async fn delete_event(&self, id: String) -> Result { + let mut state = self.state.lock().await; + + let mut person_count: u64 = state.people.len() as u64; + state.people = state + .people + .clone() + .into_iter() + .filter(|((event_id, _), _)| event_id != &id) + .collect(); + person_count -= state.people.len() as u64; + + state.events.remove(&id); + + Ok(EventDeletion { id, person_count }) + } +} + +impl MemoryAdaptor { + pub async fn new() -> Self { + println!("🧠 Using in-memory storage"); + println!("🚨 WARNING: All data will be lost when the process ends. Make sure you choose a database adaptor before deploying."); + + let state = Mutex::new(State { + stats: Stats { + event_count: 0, + person_count: 0, + }, + events: HashMap::new(), + people: HashMap::new(), + }); + + Self { state } + } +} + +#[derive(Debug)] +pub enum MemoryAdaptorError {} + +impl Display for MemoryAdaptorError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Memory adaptor error") + } +} + +impl Error for MemoryAdaptorError {} diff --git a/backend/common/src/event.rs b/backend/common/src/event.rs index 4584183..1b2fd07 100644 --- a/backend/common/src/event.rs +++ b/backend/common/src/event.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; +#[derive(Clone)] pub struct Event { pub id: String, pub name: String, @@ -9,6 +10,7 @@ pub struct Event { pub timezone: String, } +#[derive(Clone)] /// Info about a deleted event pub struct EventDeletion { pub id: String, diff --git a/backend/common/src/person.rs b/backend/common/src/person.rs index 8642cd4..fd19b76 100644 --- a/backend/common/src/person.rs +++ b/backend/common/src/person.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; +#[derive(Clone)] pub struct Person { pub name: String, pub password_hash: Option, diff --git a/backend/common/src/stats.rs b/backend/common/src/stats.rs index 7bb9ec2..a88d949 100644 --- a/backend/common/src/stats.rs +++ b/backend/common/src/stats.rs @@ -1,3 +1,4 @@ +#[derive(Clone)] pub struct Stats { pub event_count: i64, pub person_count: i64, diff --git a/backend/src/adaptors.rs b/backend/src/adaptors.rs new file mode 100644 index 0000000..95d280b --- /dev/null +++ b/backend/src/adaptors.rs @@ -0,0 +1,13 @@ +#[cfg(feature = "sql-adaptor")] +pub async fn create_adaptor() -> sql_adaptor::SqlAdaptor { + sql_adaptor::SqlAdaptor::new().await +} + +#[cfg(feature = "datastore-adaptor")] +pub async fn create_adaptor() -> datastore_adaptor::DatastoreAdaptor { + datastore_adaptor::DatastoreAdaptor::new().await +} + +pub async fn create_adaptor() -> memory_adaptor::MemoryAdaptor { + memory_adaptor::MemoryAdaptor::new().await +} diff --git a/backend/src/main.rs b/backend/src/main.rs index 10f4938..ee9924a 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -8,7 +8,6 @@ use axum::{ BoxError, Router, Server, }; use routes::*; -use sql_adaptor::SqlAdaptor; use tokio::sync::Mutex; use tower::ServiceBuilder; use tower_governor::{errors::display_error, governor::GovernorConfigBuilder, GovernorLayer}; @@ -16,8 +15,10 @@ use tower_http::{cors::CorsLayer, trace::TraceLayer}; use utoipa::OpenApi; use utoipa_swagger_ui::SwaggerUi; +use crate::adaptors::create_adaptor; use crate::docs::ApiDoc; +mod adaptors; mod docs; mod errors; mod payloads; @@ -37,7 +38,7 @@ async fn main() { dotenv::dotenv().ok(); let shared_state = Arc::new(Mutex::new(ApiState { - adaptor: SqlAdaptor::new().await, + adaptor: create_adaptor().await, })); // CORS configuration