use ciborium::Value as CborValue;
use indexmap::IndexMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use vantage_core::{Result, error};
use vantage_types::Record;
use crate::{
capabilities::{Capability, VistaCapabilities},
filter::FilterOp,
metadata::VistaMetadata,
query::VistaQuery,
reference::Reference,
};
mod calls;
mod get_only;
mod refs;
mod store;
mod table_shell;
#[cfg(test)]
mod tests;
pub use calls::MockCalls;
type Rec = Record<CborValue>;
#[derive(Clone)]
pub struct MockShell {
data: Arc<Mutex<IndexMap<String, Rec>>>,
next_auto_id: Arc<Mutex<i64>>,
fail_reads: Arc<AtomicBool>,
calls: Arc<calls::Counters>,
query: VistaQuery,
capabilities: VistaCapabilities,
metadata: VistaMetadata,
ref_targets: IndexMap<String, MockShell>,
contained: IndexMap<String, VistaMetadata>,
id_prefix: Option<String>,
gets: get_only::GetMode,
}
impl MockShell {
pub fn new() -> Self {
Self {
data: Arc::default(),
next_auto_id: Arc::new(Mutex::new(1)),
fail_reads: Arc::default(),
calls: Arc::default(),
query: VistaQuery::default(),
capabilities: [
Capability::Insert,
Capability::Update,
Capability::Delete,
Capability::Order,
Capability::Search,
Capability::FilterOps,
Capability::Window,
Capability::Traverse,
Capability::ConfineWrites,
]
.into_iter()
.collect(),
metadata: VistaMetadata::new(),
ref_targets: IndexMap::new(),
contained: IndexMap::new(),
id_prefix: None,
gets: get_only::GetMode::default(),
}
}
pub fn with_capabilities(mut self, capabilities: VistaCapabilities) -> Self {
self.capabilities = capabilities;
self
}
pub fn with_metadata(mut self, metadata: VistaMetadata) -> Self {
self.metadata = metadata;
self
}
pub fn with_ref_target(mut self, relation: impl Into<String>, target: MockShell) -> Self {
self.ref_targets.insert(relation.into(), target);
self
}
pub fn with_contained(mut self, reference: Reference, schema: VistaMetadata) -> Self {
self.contained.insert(reference.name.clone(), schema);
self.metadata = self.metadata.with_reference(reference);
self
}
pub fn with_id_prefix(mut self, prefix: impl Into<String>) -> Self {
self.id_prefix = Some(prefix.into());
self
}
pub fn with_record(self, id: impl Into<String>, record: Rec) -> Self {
self.set_record(id, record);
self
}
fn guard_reads(&self) -> Result<()> {
if self.fail_reads.load(Ordering::SeqCst) {
return Err(error!("mock source read failed (injected)"));
}
Ok(())
}
fn key(&self, id: &str) -> String {
match &self.id_prefix {
Some(prefix) if !id.starts_with(prefix.as_str()) => format!("{prefix}{id}"),
_ => id.to_string(),
}
}
fn id_field(&self) -> &str {
self.metadata.id_column.as_deref().unwrap_or("id")
}
fn in_set(&self, record: &Rec) -> Result<bool> {
self.query.matches(record, &self.metadata)
}
fn fit(&self, record: &Rec, key: &str) -> Result<Rec> {
let invariants: IndexMap<String, CborValue> = self
.query
.conditions
.iter()
.filter(|c| c.op == FilterOp::Eq)
.map(|c| (c.column.clone(), c.value.clone()))
.collect();
let mut stored = record.clone();
vantage_dataset::invariants::conform(&mut stored, &invariants)?;
stored.insert(
self.id_field().to_string(),
CborValue::Text(key.to_string()),
);
if !self.in_set(&stored)? {
return Err(error!("Record would fall outside the set", id = key).mark_conflict());
}
Ok(stored)
}
fn next_auto_id(&self) -> String {
let mut next = self.next_auto_id.lock().unwrap();
let id = next.to_string();
*next += 1;
id
}
fn matching<'a>(&self, data: &'a IndexMap<String, Rec>) -> Result<Vec<(&'a String, &'a Rec)>> {
let mut rows = Vec::new();
for (id, row) in data {
if self.in_set(row)? {
rows.push((id, row));
}
}
if self.query.order.is_some() {
rows.sort_by(|(a_id, a), (b_id, b)| {
self.query.cmp_rows((a_id, a), (b_id, b), &self.metadata)
});
}
Ok(rows)
}
fn held<'a>(&self, data: &'a IndexMap<String, Rec>, key: &str) -> Result<Option<&'a Rec>> {
match data.get(key) {
Some(row) if self.in_set(row)? => Ok(Some(row)),
_ => Ok(None),
}
}
}
impl Default for MockShell {
fn default() -> Self {
Self::new()
}
}