use vantage_core::Result;
use vantage_types::{EmptyEntity, Record};
use crate::table::{AfterFn, BeforeDeleteFn, BeforeFn, HookReturn, Phase, Table};
use crate::traits::table_source::TableSource;
pub(crate) async fn run_before<T: TableSource>(
hooks: &[(Phase, BeforeFn<T>)],
record: &mut Record<T::Value>,
table: &Table<T, EmptyEntity>,
) -> Result<()> {
for (_phase, hook) in hooks {
hook(record, table).await?;
}
Ok(())
}
pub(crate) async fn run_after<T: TableSource>(
hooks: &[AfterFn<T>],
id: &T::Id,
record: &Record<T::Value>,
table: &Table<T, EmptyEntity>,
) -> Result<()> {
for hook in hooks {
hook(id, record, table).await?;
}
Ok(())
}
pub(crate) async fn run_before_delete<T: TableSource>(
hooks: &[BeforeDeleteFn<T>],
id: &T::Id,
record: &Record<T::Value>,
table: &Table<T, EmptyEntity>,
) -> Result<HookReturn> {
for hook in hooks {
if let HookReturn::Handled = hook(id, record, table).await? {
return Ok(HookReturn::Handled);
}
}
Ok(HookReturn::Proceed)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::mocks::mock_table_source::MockTableSource;
use crate::table::{AfterFn, BeforeFn, Hook};
use serde_json::{Value, json};
use std::sync::Arc;
use std::sync::atomic::{AtomicUsize, Ordering};
use vantage_dataset::prelude::{InsertableValueSet, ReadableValueSet, WritableValueSet};
type MockTable = Table<MockTableSource, EmptyEntity>;
fn stamp<'a>(
rec: &'a mut Record<Value>,
_t: &'a MockTable,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
rec.insert("created".into(), json!("yes"));
Ok(())
})
}
#[allow(clippy::ptr_arg)]
fn veto<'a>(
_id: &'a String,
_rec: &'a Record<Value>,
_t: &'a MockTable,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<HookReturn>> + Send + 'a>> {
Box::pin(async move { Err(vantage_core::error!("deletion not allowed")) })
}
#[allow(clippy::ptr_arg)]
fn soft_delete<'a>(
id: &'a String,
_rec: &'a Record<Value>,
table: &'a MockTable,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<HookReturn>> + Send + 'a>> {
Box::pin(async move {
let mut patch = Record::new();
patch.insert("deleted".into(), json!("2026-01-01"));
table.patch_value(id.clone(), &patch).await?;
Ok(HookReturn::Handled)
})
}
#[tokio::test]
async fn before_insert_mutates_record() {
let src = MockTableSource::new().with_data("t", vec![]).await;
let table = MockTable::new("t", src)
.with_hook(Hook::BeforeInsert(Phase::Populate, Arc::new(stamp)));
let id = table
.insert_return_id_value(&Record::from(json!({"name": "a"})))
.await
.unwrap();
assert_eq!(
table.get_value(id).await.unwrap().unwrap()["created"],
json!("yes")
);
}
#[tokio::test]
async fn before_delete_vetoes() {
let src = MockTableSource::new()
.with_data("t", vec![json!({"id": "1", "name": "a"})])
.await;
let table = MockTable::new("t", src).with_hook(Hook::BeforeDelete(Arc::new(veto)));
assert!(table.delete("1").await.is_err());
assert!(table.get_value("1".to_string()).await.unwrap().is_some());
}
#[tokio::test]
async fn before_delete_handled_soft_deletes() {
let src = MockTableSource::new()
.with_data("t", vec![json!({"id": "1", "name": "a"})])
.await;
let table = MockTable::new("t", src).with_hook(Hook::BeforeDelete(Arc::new(soft_delete)));
table.delete("1").await.unwrap();
let row = table.get_value("1".to_string()).await.unwrap().unwrap();
assert_eq!(row["deleted"], json!("2026-01-01"));
}
#[tokio::test]
async fn after_delete_runs_side_effect() {
let hits = Arc::new(AtomicUsize::new(0));
let seen = hits.clone();
let hook: AfterFn<MockTableSource> = Arc::new(
move |_id: &String,
_rec: &Record<Value>,
_t: &MockTable|
-> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<()>> + Send + '_>,
> {
let seen = seen.clone();
Box::pin(async move {
seen.fetch_add(1, Ordering::SeqCst);
Ok(())
})
},
);
let src = MockTableSource::new()
.with_data("t", vec![json!({"id": "1", "name": "a"})])
.await;
let table = MockTable::new("t", src).with_hook(Hook::AfterDelete(hook));
table.delete("1").await.unwrap();
assert_eq!(hits.load(Ordering::SeqCst), 1);
}
#[tokio::test]
async fn before_hooks_run_in_phase_order() {
let order = Arc::new(std::sync::Mutex::new(Vec::<&'static str>::new()));
let recorder = |label: &'static str,
log: Arc<std::sync::Mutex<Vec<&'static str>>>|
-> BeforeFn<MockTableSource> {
Arc::new(
move |_r: &mut Record<Value>,
_t: &MockTable|
-> std::pin::Pin<
Box<dyn std::future::Future<Output = Result<()>> + Send + '_>,
> {
let log = log.clone();
Box::pin(async move {
log.lock().unwrap().push(label);
Ok(())
})
},
)
};
let src = MockTableSource::new().with_data("t", vec![]).await;
let table = MockTable::new("t", src)
.with_hook(Hook::BeforeInsert(
Phase::Validate,
recorder("validate", order.clone()),
))
.with_hook(Hook::BeforeInsert(
Phase::Normalize,
recorder("normalize", order.clone()),
));
table
.insert_return_id_value(&Record::from(json!({"name": "a"})))
.await
.unwrap();
assert_eq!(*order.lock().unwrap(), vec!["normalize", "validate"]);
}
}