use std::sync::Arc;
use ciborium::Value as CborValue;
use tokio::runtime::Handle;
use vantage_dataset::traits::ReadableValueSet;
use vantage_types::Record;
use vantage_vista_factory::VistaCatalog;
use crate::augment::Augmentation;
use crate::dio::Dio;
use crate::error::LensBuildError;
use crate::lens::CacheStatus;
use super::callbacks::{
DioCallback, DioListPageCallback, DioLoadDetailCallback, boxed_dio_callback,
boxed_list_page_callback, boxed_load_detail_callback,
};
use super::{Lens, LensBuilder, LensCallbacks};
impl LensBuilder {
pub fn build(self) -> Result<Lens, LensBuildError> {
if let Some(err) = self.deferred_cache_error {
return Err(err);
}
let cache_source = self
.cache_source
.ok_or(LensBuildError::MissingCacheSource)?;
let runtime = self
.runtime
.unwrap_or_else(|| Handle::try_current().expect("LensBuilder::build called outside a tokio runtime; supply one with .runtime(handle)"));
let mut on_list_page = self.on_list_page;
let mut on_load_detail = self.on_load_detail;
let mut on_refresh = self.on_refresh;
if !self.augmentations.is_empty() && on_load_detail.is_none() {
let catalog = self.catalog.ok_or(LensBuildError::MissingCatalog)?;
let augmentations = Arc::new(self.augmentations);
if on_list_page.is_none() {
on_list_page = Some(synth_list_page());
}
if on_refresh.is_none() {
on_refresh = Some(synth_refresh());
}
on_load_detail = Some(synth_load_detail(catalog, augmentations));
}
let callbacks = LensCallbacks {
on_start: self.on_start,
on_refresh,
on_write: self.on_write,
on_event: self.on_event,
on_query: self.on_query,
total_provider: self.total_provider,
on_load_chunk: self.on_load_chunk,
on_list_page,
on_load_detail,
};
Ok(Lens {
cache_source,
callbacks: Arc::new(callbacks),
defaults: self.defaults,
runtime,
activity: self.activity,
})
}
}
fn synth_list_page() -> DioListPageCallback {
boxed_list_page_callback(move |dio: &Dio, desc| {
let dio = dio.clone();
async move {
let master = dio.master();
if master.capabilities().can_fetch_window {
master.fetch_window(desc.offset, desc.limit).await
} else {
use vantage_dataset::traits::ReadableValueSet;
let rows = master.list_values().await?;
Ok(rows
.into_iter()
.skip(desc.offset)
.take(desc.limit)
.collect())
}
}
})
}
fn synth_load_detail(
catalog: Arc<VistaCatalog>,
augmentations: Arc<Vec<Augmentation>>,
) -> DioLoadDetailCallback {
boxed_load_detail_callback(move |dio: &Dio, id| {
let dio = dio.clone();
let catalog = catalog.clone();
let augmentations = augmentations.clone();
async move {
let mut row = dio.cache().get_value(&id).await?.unwrap_or_default();
let master_id_column = dio.master().get_id_column().unwrap_or("id").to_string();
for aug in augmentations.iter() {
aug.augment_row(&master_id_column, &mut row, &catalog)
.await?;
}
Ok(row)
}
})
}
fn synth_refresh() -> DioCallback {
boxed_dio_callback(move |dio: &Dio| {
let dio = dio.clone();
async move {
let fresh = dio.master().list_values().await?;
let cache = dio.cache();
let existing = cache.list_values_with_status().await?;
for (id, list_row) in &fresh {
match existing.get(id) {
Some((old, _)) => {
if !list_fields_changed(list_row, old) {
continue;
}
let mut merged = old.clone();
for (k, v) in list_row {
merged.insert(k.clone(), v.clone());
}
cache
.insert_value_with_status(id, &merged, CacheStatus::Incomplete)
.await?;
}
None => {
cache
.insert_value_with_status(id, list_row, CacheStatus::Incomplete)
.await?;
}
}
}
for id in existing.keys() {
if !fresh.contains_key(id) {
cache.delete_value(id).await?;
}
}
Ok(())
}
})
}
fn list_fields_changed(list_row: &Record<CborValue>, cached: &Record<CborValue>) -> bool {
list_row.iter().any(|(k, v)| cached.get(k) != Some(v))
}
#[cfg(test)]
mod tests {
use super::list_fields_changed;
use ciborium::Value;
use vantage_types::Record;
fn rec(pairs: &[(&str, Value)]) -> Record<Value> {
pairs
.iter()
.map(|(k, v)| (k.to_string(), v.clone()))
.collect()
}
#[test]
fn unchanged_list_fields_ignore_augmented_columns() {
let list = rec(&[
("status", Value::from("LIVE")),
("updated_at", Value::from("t1")),
]);
let cached = rec(&[
("status", Value::from("LIVE")),
("updated_at", Value::from("t1")),
("subject_area", Value::from("finance")),
]);
assert!(!list_fields_changed(&list, &cached));
}
#[test]
fn a_changed_non_augmented_field_is_detected() {
let list = rec(&[
("status", Value::from("DECOMMISSIONED")),
("updated_at", Value::from("t2")),
]);
let cached = rec(&[
("status", Value::from("LIVE")),
("updated_at", Value::from("t1")),
("subject_area", Value::from("finance")),
]);
assert!(list_fields_changed(&list, &cached));
}
#[test]
fn a_new_list_key_absent_from_cache_counts_as_changed() {
let list = rec(&[
("status", Value::from("LIVE")),
("region", Value::from("eu")),
]);
let cached = rec(&[("status", Value::from("LIVE"))]);
assert!(list_fields_changed(&list, &cached));
}
}