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",
]
[[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"

View file

@ -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"] }

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 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<Stats, Self::Error> {
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<i64, Self::Error> {
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<Option<Vec<Person>>, Self::Error> {
@ -67,7 +71,7 @@ impl Adaptor for DatastoreAdaptor {
// Check the event exists
if client
.get::<Value, _>(Key::new(EVENT_KIND).id(event_id.clone()))
.get::<DatastoreEvent, _>(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::<Value, _>(key.clone()).await?;
let existing_event = client.get::<DatastoreEvent, _>(key.clone()).await?;
// Mark as visited if it exists
if let Some(mut event) = existing_event
.clone()
.map(HashMap::<String, Value>::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<Event, Self::Error> {
@ -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<EventDeletion, Self::Error> {
let mut client = self.client.lock().await;
let mut people_keys: Vec<Key> = client
let mut keys_to_delete: Vec<Key> = 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<Client>, id: &str) -> Result<i64, DatastoreAdaptorError> {
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<Person, DatastoreAdaptorError> {
let person: HashMap<String, Value> = 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<String>,
timezone: String,
}
fn parse_into_event(id: String, value: Value) -> Result<Event, DatastoreAdaptorError> {
let event: HashMap<String, Value> = 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<String>,
created: i64,
eventId: String,
availability: Vec<String>,
}
impl From<DatastorePerson> 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<Event> 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<Utc> {
DateTime::from_utc(NaiveDateTime::from_timestamp_opt(unix, 0).unwrap(), Utc)
}
#[derive(Debug)]