use std::collections::BTreeMap;
use std::future::Future;
use std::pin::Pin;
use std::sync::Mutex;
use super::model::{CapsuleModel, DataCapsuleError, FieldSpec, ModelManifest, Record, value_key};
pub type CapsuleFuture<'a, T> =
Pin<Box<dyn Future<Output = Result<T, DataCapsuleError>> + Send + 'a>>;
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub struct ImportBatch<'a> {
pub model: &'a ModelManifest,
pub records: &'a [Record],
}
impl<'a> ImportBatch<'a> {
#[must_use]
pub const fn new(model: &'a ModelManifest, records: &'a [Record]) -> Self {
Self { model, records }
}
}
pub type ModelData = (Vec<FieldSpec>, Vec<Record>);
pub trait CapsuleStore: Send + Sync {
fn describe<'a>(&'a self, model: &'a CapsuleModel) -> CapsuleFuture<'a, Vec<FieldSpec>>;
fn fetch<'a>(
&'a self,
model: &'a CapsuleModel,
subject: &'a str,
) -> CapsuleFuture<'a, Vec<Record>>;
fn fetch_subject<'a>(
&'a self,
models: &'a [CapsuleModel],
subject: &'a str,
) -> CapsuleFuture<'a, Vec<ModelData>> {
Box::pin(async move {
let mut data = Vec::with_capacity(models.len());
for model in models {
data.push((
self.describe(model).await?,
self.fetch(model, subject).await?,
));
}
Ok(data)
})
}
fn insert_all<'a>(&'a self, batches: &'a [ImportBatch<'a>]) -> CapsuleFuture<'a, ()>;
}
#[derive(Debug, Default)]
struct MemoryTable {
fields: Vec<FieldSpec>,
rows: Vec<Record>,
}
#[derive(Debug, Default)]
pub struct MemoryCapsuleStore {
tables: Mutex<BTreeMap<String, MemoryTable>>,
}
impl MemoryCapsuleStore {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn table(self, name: impl Into<String>, fields: Vec<FieldSpec>) -> Self {
self.lock().insert(
name.into(),
MemoryTable {
fields,
rows: Vec::new(),
},
);
self
}
pub fn insert(&self, table: &str, row: serde_json::Value) {
let serde_json::Value::Object(row) = row else {
panic!("MemoryCapsuleStore::insert needs a JSON object");
};
self.lock()
.get_mut(table)
.unwrap_or_else(|| panic!("MemoryCapsuleStore has no table {table:?}"))
.rows
.push(row);
}
#[must_use]
pub fn rows(&self, table: &str) -> Vec<Record> {
self.lock()
.get(table)
.map(|t| t.rows.clone())
.unwrap_or_default()
}
fn lock(&self) -> std::sync::MutexGuard<'_, BTreeMap<String, MemoryTable>> {
self.tables
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}
}
fn unknown(table: &str) -> DataCapsuleError {
DataCapsuleError::Store(format!("unknown table {table:?}"))
}
fn key_of(row: &Record, column: &str) -> Option<String> {
row.get(column).and_then(value_key)
}
impl CapsuleStore for MemoryCapsuleStore {
fn describe<'a>(&'a self, model: &'a CapsuleModel) -> CapsuleFuture<'a, Vec<FieldSpec>> {
let result = self
.lock()
.get(&model.table)
.map(|t| t.fields.clone())
.ok_or_else(|| unknown(&model.table));
Box::pin(async move { result })
}
fn fetch<'a>(
&'a self,
model: &'a CapsuleModel,
subject: &'a str,
) -> CapsuleFuture<'a, Vec<Record>> {
let result = self
.lock()
.get(&model.table)
.map(|t| {
t.rows
.iter()
.filter(|row| key_of(row, &model.subject_column).as_deref() == Some(subject))
.cloned()
.collect()
})
.ok_or_else(|| unknown(&model.table));
Box::pin(async move { result })
}
fn insert_all<'a>(&'a self, batches: &'a [ImportBatch<'a>]) -> CapsuleFuture<'a, ()> {
let result = (|| {
let mut tables = self.lock();
for batch in batches {
let table = tables
.get(&batch.model.table)
.ok_or_else(|| unknown(&batch.model.table))?;
let pk = batch.model.primary_key.as_str();
let mut seen: std::collections::BTreeSet<String> =
table.rows.iter().filter_map(|r| key_of(r, pk)).collect();
for record in batch.records {
if let Some(key) = key_of(record, pk)
&& !seen.insert(key.clone())
{
return Err(DataCapsuleError::Conflict(format!(
"{}.{pk} = {key} already exists",
batch.model.table
)));
}
}
}
for batch in batches {
if let Some(table) = tables.get_mut(&batch.model.table) {
table.rows.extend(batch.records.iter().cloned());
}
}
Ok(())
})();
Box::pin(async move { result })
}
}