From e13f46678520d3ae51bfdc7565a49620c9cf6187 Mon Sep 17 00:00:00 2001 From: Ben Grant Date: Mon, 15 May 2023 22:53:50 +1000 Subject: [PATCH] Support Null values and use structs --- backend/Cargo.lock | 61 +++++- backend/adaptors/datastore/Cargo.toml | 2 +- backend/adaptors/datastore/src/lib.rs | 263 +++++++++++--------------- 3 files changed, 168 insertions(+), 158 deletions(-) diff --git a/backend/Cargo.lock b/backend/Cargo.lock index 04eb650..0a5fd77 100644 --- a/backend/Cargo.lock +++ b/backend/Cargo.lock @@ -746,6 +746,41 @@ dependencies = [ "syn 2.0.15", ] +[[package]] +name = "darling" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0c960ae2da4de88a91b2d920c2a7233b400bc33cb28453a2987822d8392519b" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b5a2f4ac4969822c62224815d069952656cadc7084fdca9751e6d959189b72" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + [[package]] name = "dashmap" version = "5.4.0" @@ -1087,11 +1122,11 @@ dependencies = [ [[package]] name = "google-cloud" version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a517f0235af652d334a021b81aa2e8f18a77512c26be18722debb7d405912f80" +source = "git+https://github.com/GRA0007/google-cloud-rs.git#5b2c3d6dcde9e58528c90c0a3016f123104c5fe3" dependencies = [ "chrono", "futures", + "google-cloud-derive", "http", "hyper", "hyper-rustls", @@ -1106,6 +1141,16 @@ dependencies = [ "tonic-build", ] +[[package]] +name = "google-cloud-derive" +version = "0.2.1" +source = "git+https://github.com/GRA0007/google-cloud-rs.git#5b2c3d6dcde9e58528c90c0a3016f123104c5fe3" +dependencies = [ + "darling", + "quote", + "syn 1.0.109", +] + [[package]] name = "governor" version = "0.5.1" @@ -1357,6 +1402,12 @@ dependencies = [ "cxx-build", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "0.3.0" @@ -2981,6 +3032,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "strsim" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c" + [[package]] name = "subtle" version = "2.4.1" diff --git a/backend/adaptors/datastore/Cargo.toml b/backend/adaptors/datastore/Cargo.toml index f9a69ac..bc6764b 100644 --- a/backend/adaptors/datastore/Cargo.toml +++ b/backend/adaptors/datastore/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" async-trait = "0.1.68" chrono = "0.4.24" common = { path = "../../common" } -google-cloud = { version = "0.2.1", features = ["datastore"] } +google-cloud = { git = "https://github.com/GRA0007/google-cloud-rs.git", features = ["datastore", "derive"] } serde = "1.0.163" serde_json = "1.0.96" tokio = { version = "1.28.1", features = ["rt-multi-thread"] } diff --git a/backend/adaptors/datastore/src/lib.rs b/backend/adaptors/datastore/src/lib.rs index 21622b0..3d4e33c 100644 --- a/backend/adaptors/datastore/src/lib.rs +++ b/backend/adaptors/datastore/src/lib.rs @@ -1,4 +1,4 @@ -use std::{collections::HashMap, env, error::Error, fmt::Display}; +use std::{env, error::Error, fmt::Display}; use async_trait::async_trait; use chrono::{DateTime, NaiveDateTime, Utc}; @@ -10,8 +10,7 @@ use common::{ }; use google_cloud::{ authorize::ApplicationCredentials, - datastore::{Client, Filter, FromValue, IntoValue, Key, Query, Value}, - error::ConvertError, + datastore::{Client, Filter, FromValue, IntoValue, Key, Query}, }; use tokio::sync::Mutex; @@ -31,12 +30,17 @@ impl Adaptor for DatastoreAdaptor { type Error = DatastoreAdaptorError; async fn get_stats(&self) -> Result { - let event_count = get_stats_value(&self.client, STATS_EVENTS_ID).await?; - let person_count = get_stats_value(&self.client, STATS_PEOPLE_ID).await?; + let mut client = self.client.lock().await; + + let event_key = Key::new(STATS_KIND).id(STATS_EVENTS_ID); + let event_stats: DatastoreStats = client.get(event_key).await?.unwrap_or_default(); + + let person_key = Key::new(STATS_KIND).id(STATS_PEOPLE_ID); + let person_stats: DatastoreStats = client.get(person_key).await?.unwrap_or_default(); Ok(Stats { - event_count, - person_count, + event_count: event_stats.value, + person_count: person_stats.value, }) } @@ -44,22 +48,22 @@ impl Adaptor for DatastoreAdaptor { let mut client = self.client.lock().await; let key = Key::new(STATS_KIND).id(STATS_EVENTS_ID); - let event_count = get_stats_value(&self.client, STATS_EVENTS_ID).await? + 1; + let mut event_stats: DatastoreStats = client.get(key.clone()).await?.unwrap_or_default(); - let updated_props = HashMap::from([(String::from("value"), event_count.into_value())]); - client.put((key, updated_props)).await?; - Ok(event_count) + event_stats.value += 1; + client.put((key, event_stats.clone())).await?; + Ok(event_stats.value) } async fn increment_stat_person_count(&self) -> Result { let mut client = self.client.lock().await; let key = Key::new(STATS_KIND).id(STATS_PEOPLE_ID); - let person_count = get_stats_value(&self.client, STATS_PEOPLE_ID).await? + 1; + let mut person_stats: DatastoreStats = client.get(key.clone()).await?.unwrap_or_default(); - let updated_props = HashMap::from([(String::from("value"), person_count.into_value())]); - client.put((key, updated_props)).await?; - Ok(person_count) + person_stats.value += 1; + client.put((key, person_stats.clone())).await?; + Ok(person_stats.value) } async fn get_people(&self, event_id: String) -> Result>, Self::Error> { @@ -67,7 +71,7 @@ impl Adaptor for DatastoreAdaptor { // Check the event exists if client - .get::(Key::new(EVENT_KIND).id(event_id.clone())) + .get::(Key::new(EVENT_KIND).id(event_id.clone())) .await? .is_none() { @@ -82,7 +86,11 @@ impl Adaptor for DatastoreAdaptor { ) .await? .into_iter() - .filter_map(|entity| parse_into_person(entity.properties().clone()).ok()) + .filter_map(|entity| { + DatastorePerson::from_value(entity.properties().clone()) + .ok() + .map(|ds_person| ds_person.into()) + }) .collect(), )) } @@ -110,22 +118,9 @@ impl Adaptor for DatastoreAdaptor { key = entity.key().clone(); } - let mut properties = HashMap::new(); - properties.insert(String::from("name"), person.name.clone().into_value()); - if let Some(password_hash) = person.password_hash.clone() { - properties.insert(String::from("password"), password_hash.into_value()); - } - properties.insert(String::from("eventId"), event_id.into_value()); - properties.insert( - String::from("created"), - person.created_at.clone().timestamp().into_value(), - ); - properties.insert( - String::from("availability"), - person.availability.clone().into_value(), - ); - - client.put((key, properties)).await?; + client + .put((key, DatastorePerson::from_person(person.clone(), event_id))) + .await?; Ok(person) } @@ -134,21 +129,15 @@ impl Adaptor for DatastoreAdaptor { let mut client = self.client.lock().await; let key = Key::new(EVENT_KIND).id(id.clone()); - let existing_event = client.get::(key.clone()).await?; + let existing_event = client.get::(key.clone()).await?; // Mark as visited if it exists - if let Some(mut event) = existing_event - .clone() - .map(HashMap::::from_value) - .transpose()? - { - event.insert(String::from("visited"), Utc::now().timestamp().into_value()); + if let Some(mut event) = existing_event.clone() { + event.visited = Utc::now().timestamp(); client.put((key, event)).await?; } - Ok(existing_event - .map(|value| parse_into_event(id, value)) - .transpose()?) + Ok(existing_event.map(|e| e.to_event(id))) } async fn create_event(&self, event: Event) -> Result { @@ -156,23 +145,8 @@ impl Adaptor for DatastoreAdaptor { let key = Key::new(EVENT_KIND).id(event.id.clone()); - let mut properties = HashMap::new(); - properties.insert(String::from("name"), event.name.clone().into_value()); - properties.insert( - String::from("created"), - event.created_at.clone().timestamp().into_value(), - ); - properties.insert( - String::from("visited"), - event.visited_at.clone().timestamp().into_value(), - ); - properties.insert(String::from("times"), event.times.clone().into_value()); - properties.insert( - String::from("timezone"), - event.timezone.clone().into_value(), - ); - - client.put((key, properties)).await?; + let ds_event: DatastoreEvent = event.clone().into(); + client.put((key, ds_event)).await?; Ok(event) } @@ -180,7 +154,7 @@ impl Adaptor for DatastoreAdaptor { async fn delete_event(&self, id: String) -> Result { let mut client = self.client.lock().await; - let mut people_keys: Vec = client + let mut keys_to_delete: Vec = client .query( Query::new(PERSON_KIND) .filter(Filter::Equal("eventId".into(), id.clone().into_value())), @@ -190,10 +164,10 @@ impl Adaptor for DatastoreAdaptor { .map(|entity| entity.key().clone()) .collect(); - let person_count = people_keys.len().try_into().unwrap(); - people_keys.insert(0, Key::new(EVENT_KIND).id(id.clone())); + let person_count = keys_to_delete.len().try_into().unwrap(); + keys_to_delete.insert(0, Key::new(EVENT_KIND).id(id.clone())); - client.delete_all(people_keys).await?; + client.delete_all(keys_to_delete).await?; Ok(EventDeletion { id, person_count }) } @@ -222,101 +196,80 @@ impl DatastoreAdaptor { } } -async fn get_stats_value(client: &Mutex, id: &str) -> Result { - let mut client = client.lock().await; - Ok(client - .get(Key::new(STATS_KIND).id(id)) - .await? - .unwrap_or(HashMap::from([(String::from("value"), 0)])) - .get("value") - .cloned() - .unwrap_or(0)) +#[derive(FromValue, IntoValue, Default, Clone)] +struct DatastoreStats { + value: i64, } -fn parse_into_person(value: Value) -> Result { - let person: HashMap = HashMap::from_value(value)?; - Ok(Person { - name: String::from_value( - person - .get("name") - .ok_or(ConvertError::MissingProperty("name".to_owned()))? - .clone(), - )?, - password_hash: person - .get("password") - .map(|p| String::from_value(p.clone())) - .transpose()?, - created_at: DateTime::from_utc( - NaiveDateTime::from_timestamp_opt( - i64::from_value( - person - .get("created") - .ok_or(ConvertError::MissingProperty("created".to_owned()))? - .clone(), - )?, - 0, - ) - .unwrap(), - Utc, - ), - availability: Vec::from_value( - person - .get("availability") - .ok_or(ConvertError::MissingProperty("availability".to_owned()))? - .clone(), - )?, - }) +#[derive(FromValue, IntoValue, Clone)] +struct DatastoreEvent { + name: String, + created: i64, + visited: i64, + times: Vec, + timezone: String, } -fn parse_into_event(id: String, value: Value) -> Result { - let event: HashMap = HashMap::from_value(value)?; - Ok(Event { - id, - name: String::from_value( - event - .get("name") - .ok_or(ConvertError::MissingProperty("name".to_owned()))? - .clone(), - )?, - created_at: DateTime::from_utc( - NaiveDateTime::from_timestamp_opt( - i64::from_value( - event - .get("created") - .ok_or(ConvertError::MissingProperty("created".to_owned()))? - .clone(), - )?, - 0, - ) - .unwrap(), - Utc, - ), - visited_at: DateTime::from_utc( - NaiveDateTime::from_timestamp_opt( - i64::from_value( - event - .get("visited") - .ok_or(ConvertError::MissingProperty("visited".to_owned()))? - .clone(), - )?, - 0, - ) - .unwrap(), - Utc, - ), - times: Vec::from_value( - event - .get("times") - .ok_or(ConvertError::MissingProperty("times".to_owned()))? - .clone(), - )?, - timezone: String::from_value( - event - .get("timezone") - .ok_or(ConvertError::MissingProperty("timezone".to_owned()))? - .clone(), - )?, - }) +#[derive(FromValue, IntoValue)] +#[allow(non_snake_case)] +struct DatastorePerson { + name: String, + password: Option, + created: i64, + eventId: String, + availability: Vec, +} + +impl From for Person { + fn from(value: DatastorePerson) -> Self { + Self { + name: value.name, + password_hash: value.password, + created_at: unix_to_date(value.created), + availability: value.availability, + } + } +} + +impl DatastorePerson { + fn from_person(person: Person, event_id: String) -> Self { + Self { + name: person.name, + password: person.password_hash, + created: person.created_at.timestamp(), + eventId: event_id, + availability: person.availability, + } + } +} + +impl From for DatastoreEvent { + fn from(value: Event) -> Self { + Self { + name: value.name, + created: value.created_at.timestamp(), + visited: value.visited_at.timestamp(), + times: value.times, + timezone: value.timezone, + } + } +} + +impl DatastoreEvent { + fn to_event(&self, event_id: String) -> Event { + Event { + id: event_id, + name: self.name.clone(), + created_at: unix_to_date(self.created), + visited_at: unix_to_date(self.visited), + times: self.times.clone(), + timezone: self.timezone.clone(), + } + } +} + +fn unix_to_date(unix: i64) -> DateTime { + DateTime::from_utc(NaiveDateTime::from_timestamp_opt(unix, 0).unwrap(), Utc) } #[derive(Debug)]