Support Null values and use structs

This commit is contained in:
Ben Grant 2023-05-15 22:53:50 +10:00
parent 6b99fe1c72
commit e13f466785
3 changed files with 168 additions and 158 deletions

61
backend/Cargo.lock generated
View file

@ -746,6 +746,41 @@ dependencies = [
"syn 2.0.15", "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]] [[package]]
name = "dashmap" name = "dashmap"
version = "5.4.0" version = "5.4.0"
@ -1087,11 +1122,11 @@ dependencies = [
[[package]] [[package]]
name = "google-cloud" name = "google-cloud"
version = "0.2.1" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "git+https://github.com/GRA0007/google-cloud-rs.git#5b2c3d6dcde9e58528c90c0a3016f123104c5fe3"
checksum = "a517f0235af652d334a021b81aa2e8f18a77512c26be18722debb7d405912f80"
dependencies = [ dependencies = [
"chrono", "chrono",
"futures", "futures",
"google-cloud-derive",
"http", "http",
"hyper", "hyper",
"hyper-rustls", "hyper-rustls",
@ -1106,6 +1141,16 @@ dependencies = [
"tonic-build", "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]] [[package]]
name = "governor" name = "governor"
version = "0.5.1" version = "0.5.1"
@ -1357,6 +1402,12 @@ dependencies = [
"cxx-build", "cxx-build",
] ]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]] [[package]]
name = "idna" name = "idna"
version = "0.3.0" version = "0.3.0"
@ -2981,6 +3032,12 @@ dependencies = [
"unicode-normalization", "unicode-normalization",
] ]
[[package]]
name = "strsim"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6446ced80d6c486436db5c078dde11a9f73d42b57fb273121e160b84f63d894c"
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.4.1" version = "2.4.1"

View file

@ -7,7 +7,7 @@ edition = "2021"
async-trait = "0.1.68" async-trait = "0.1.68"
chrono = "0.4.24" chrono = "0.4.24"
common = { path = "../../common" } 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 = "1.0.163"
serde_json = "1.0.96" serde_json = "1.0.96"
tokio = { version = "1.28.1", features = ["rt-multi-thread"] } tokio = { version = "1.28.1", features = ["rt-multi-thread"] }

View file

@ -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 async_trait::async_trait;
use chrono::{DateTime, NaiveDateTime, Utc}; use chrono::{DateTime, NaiveDateTime, Utc};
@ -10,8 +10,7 @@ use common::{
}; };
use google_cloud::{ use google_cloud::{
authorize::ApplicationCredentials, authorize::ApplicationCredentials,
datastore::{Client, Filter, FromValue, IntoValue, Key, Query, Value}, datastore::{Client, Filter, FromValue, IntoValue, Key, Query},
error::ConvertError,
}; };
use tokio::sync::Mutex; use tokio::sync::Mutex;
@ -31,12 +30,17 @@ impl Adaptor for DatastoreAdaptor {
type Error = DatastoreAdaptorError; type Error = DatastoreAdaptorError;
async fn get_stats(&self) -> Result<Stats, Self::Error> { async fn get_stats(&self) -> Result<Stats, Self::Error> {
let event_count = get_stats_value(&self.client, STATS_EVENTS_ID).await?; let mut client = self.client.lock().await;
let person_count = get_stats_value(&self.client, STATS_PEOPLE_ID).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 { Ok(Stats {
event_count, event_count: event_stats.value,
person_count, person_count: person_stats.value,
}) })
} }
@ -44,22 +48,22 @@ impl Adaptor for DatastoreAdaptor {
let mut client = self.client.lock().await; let mut client = self.client.lock().await;
let key = Key::new(STATS_KIND).id(STATS_EVENTS_ID); 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())]); event_stats.value += 1;
client.put((key, updated_props)).await?; client.put((key, event_stats.clone())).await?;
Ok(event_count) Ok(event_stats.value)
} }
async fn increment_stat_person_count(&self) -> Result<i64, Self::Error> { async fn increment_stat_person_count(&self) -> Result<i64, Self::Error> {
let mut client = self.client.lock().await; let mut client = self.client.lock().await;
let key = Key::new(STATS_KIND).id(STATS_PEOPLE_ID); 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())]); person_stats.value += 1;
client.put((key, updated_props)).await?; client.put((key, person_stats.clone())).await?;
Ok(person_count) Ok(person_stats.value)
} }
async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Self::Error> { async fn get_people(&self, event_id: String) -> Result<Option<Vec<Person>>, Self::Error> {
@ -67,7 +71,7 @@ impl Adaptor for DatastoreAdaptor {
// Check the event exists // Check the event exists
if client if client
.get::<Value, _>(Key::new(EVENT_KIND).id(event_id.clone())) .get::<DatastoreEvent, _>(Key::new(EVENT_KIND).id(event_id.clone()))
.await? .await?
.is_none() .is_none()
{ {
@ -82,7 +86,11 @@ impl Adaptor for DatastoreAdaptor {
) )
.await? .await?
.into_iter() .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(), .collect(),
)) ))
} }
@ -110,22 +118,9 @@ impl Adaptor for DatastoreAdaptor {
key = entity.key().clone(); key = entity.key().clone();
} }
let mut properties = HashMap::new(); client
properties.insert(String::from("name"), person.name.clone().into_value()); .put((key, DatastorePerson::from_person(person.clone(), event_id)))
if let Some(password_hash) = person.password_hash.clone() { .await?;
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?;
Ok(person) Ok(person)
} }
@ -134,21 +129,15 @@ impl Adaptor for DatastoreAdaptor {
let mut client = self.client.lock().await; let mut client = self.client.lock().await;
let key = Key::new(EVENT_KIND).id(id.clone()); let key = Key::new(EVENT_KIND).id(id.clone());
let existing_event = client.get::<Value, _>(key.clone()).await?; let existing_event = client.get::<DatastoreEvent, _>(key.clone()).await?;
// Mark as visited if it exists // Mark as visited if it exists
if let Some(mut event) = existing_event if let Some(mut event) = existing_event.clone() {
.clone() event.visited = Utc::now().timestamp();
.map(HashMap::<String, Value>::from_value)
.transpose()?
{
event.insert(String::from("visited"), Utc::now().timestamp().into_value());
client.put((key, event)).await?; client.put((key, event)).await?;
} }
Ok(existing_event Ok(existing_event.map(|e| e.to_event(id)))
.map(|value| parse_into_event(id, value))
.transpose()?)
} }
async fn create_event(&self, event: Event) -> Result<Event, Self::Error> { async fn create_event(&self, event: Event) -> Result<Event, Self::Error> {
@ -156,23 +145,8 @@ impl Adaptor for DatastoreAdaptor {
let key = Key::new(EVENT_KIND).id(event.id.clone()); let key = Key::new(EVENT_KIND).id(event.id.clone());
let mut properties = HashMap::new(); let ds_event: DatastoreEvent = event.clone().into();
properties.insert(String::from("name"), event.name.clone().into_value()); client.put((key, ds_event)).await?;
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?;
Ok(event) Ok(event)
} }
@ -180,7 +154,7 @@ impl Adaptor for DatastoreAdaptor {
async fn delete_event(&self, id: String) -> Result<EventDeletion, Self::Error> { async fn delete_event(&self, id: String) -> Result<EventDeletion, Self::Error> {
let mut client = self.client.lock().await; let mut client = self.client.lock().await;
let mut people_keys: Vec<Key> = client let mut keys_to_delete: Vec<Key> = client
.query( .query(
Query::new(PERSON_KIND) Query::new(PERSON_KIND)
.filter(Filter::Equal("eventId".into(), id.clone().into_value())), .filter(Filter::Equal("eventId".into(), id.clone().into_value())),
@ -190,10 +164,10 @@ impl Adaptor for DatastoreAdaptor {
.map(|entity| entity.key().clone()) .map(|entity| entity.key().clone())
.collect(); .collect();
let person_count = people_keys.len().try_into().unwrap(); let person_count = keys_to_delete.len().try_into().unwrap();
people_keys.insert(0, Key::new(EVENT_KIND).id(id.clone())); 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 }) Ok(EventDeletion { id, person_count })
} }
@ -222,101 +196,80 @@ impl DatastoreAdaptor {
} }
} }
async fn get_stats_value(client: &Mutex<Client>, id: &str) -> Result<i64, DatastoreAdaptorError> { #[derive(FromValue, IntoValue, Default, Clone)]
let mut client = client.lock().await; struct DatastoreStats {
Ok(client value: i64,
.get(Key::new(STATS_KIND).id(id))
.await?
.unwrap_or(HashMap::from([(String::from("value"), 0)]))
.get("value")
.cloned()
.unwrap_or(0))
} }
fn parse_into_person(value: Value) -> Result<Person, DatastoreAdaptorError> { #[derive(FromValue, IntoValue, Clone)]
let person: HashMap<String, Value> = HashMap::from_value(value)?; struct DatastoreEvent {
Ok(Person { name: String,
name: String::from_value( created: i64,
person visited: i64,
.get("name") times: Vec<String>,
.ok_or(ConvertError::MissingProperty("name".to_owned()))? timezone: String,
.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(),
)?,
})
} }
fn parse_into_event(id: String, value: Value) -> Result<Event, DatastoreAdaptorError> { #[derive(FromValue, IntoValue)]
let event: HashMap<String, Value> = HashMap::from_value(value)?; #[allow(non_snake_case)]
Ok(Event { struct DatastorePerson {
id, name: String,
name: String::from_value( password: Option<String>,
event created: i64,
.get("name") eventId: String,
.ok_or(ConvertError::MissingProperty("name".to_owned()))? availability: Vec<String>,
.clone(), }
)?,
created_at: DateTime::from_utc( impl From<DatastorePerson> for Person {
NaiveDateTime::from_timestamp_opt( fn from(value: DatastorePerson) -> Self {
i64::from_value( Self {
event name: value.name,
.get("created") password_hash: value.password,
.ok_or(ConvertError::MissingProperty("created".to_owned()))? created_at: unix_to_date(value.created),
.clone(), availability: value.availability,
)?, }
0, }
) }
.unwrap(),
Utc, impl DatastorePerson {
), fn from_person(person: Person, event_id: String) -> Self {
visited_at: DateTime::from_utc( Self {
NaiveDateTime::from_timestamp_opt( name: person.name,
i64::from_value( password: person.password_hash,
event created: person.created_at.timestamp(),
.get("visited") eventId: event_id,
.ok_or(ConvertError::MissingProperty("visited".to_owned()))? availability: person.availability,
.clone(), }
)?, }
0, }
)
.unwrap(), impl From<Event> for DatastoreEvent {
Utc, fn from(value: Event) -> Self {
), Self {
times: Vec::from_value( name: value.name,
event created: value.created_at.timestamp(),
.get("times") visited: value.visited_at.timestamp(),
.ok_or(ConvertError::MissingProperty("times".to_owned()))? times: value.times,
.clone(), timezone: value.timezone,
)?, }
timezone: String::from_value( }
event }
.get("timezone")
.ok_or(ConvertError::MissingProperty("timezone".to_owned()))? impl DatastoreEvent {
.clone(), 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<Utc> {
DateTime::from_utc(NaiveDateTime::from_timestamp_opt(unix, 0).unwrap(), Utc)
} }
#[derive(Debug)] #[derive(Debug)]