vantage_table/table/sets/
hooks.rs1use vantage_core::Result;
10use vantage_types::{EmptyEntity, Entity, InvariantValue, Record};
11
12use super::invariants::enforce_invariants;
13use crate::table::{AfterFn, BeforeDeleteFn, BeforeFn, HookReturn, Phase, Table};
14use crate::traits::table_source::TableSource;
15
16impl<T: TableSource, E: Entity<T::Value>> Table<T, E>
20where
21 T::Value: InvariantValue,
22{
23 pub async fn prepare_insert(&self, record: &Record<T::Value>) -> Result<Record<T::Value>> {
28 self.require_confined_writes("insert")?;
29 let mut record = record.clone();
30 run_before(
31 self.before_insert_hooks(),
32 &mut record,
33 self.as_entity_erased(),
34 )
35 .await?;
36 self.strip_imported_columns(&mut record);
37 enforce_invariants(&mut record, self.invariants())?;
38 Ok(record)
39 }
40
41 pub async fn finish_insert(&self, id: &T::Id, record: &Record<T::Value>) -> Result<()> {
44 run_after(
45 self.after_insert_hooks(),
46 id,
47 record,
48 self.as_entity_erased(),
49 )
50 .await
51 }
52}
53
54pub(crate) async fn run_before<T: TableSource>(
55 hooks: &[(Phase, BeforeFn<T>)],
56 record: &mut Record<T::Value>,
57 table: &Table<T, EmptyEntity>,
58) -> Result<()> {
59 for (_phase, hook) in hooks {
60 hook(record, table).await?;
61 }
62 Ok(())
63}
64
65pub(crate) async fn run_after<T: TableSource>(
66 hooks: &[AfterFn<T>],
67 id: &T::Id,
68 record: &Record<T::Value>,
69 table: &Table<T, EmptyEntity>,
70) -> Result<()> {
71 for hook in hooks {
72 hook(id, record, table).await?;
73 }
74 Ok(())
75}
76
77pub(crate) async fn run_before_delete<T: TableSource>(
80 hooks: &[BeforeDeleteFn<T>],
81 id: &T::Id,
82 record: &Record<T::Value>,
83 table: &Table<T, EmptyEntity>,
84) -> Result<HookReturn> {
85 for hook in hooks {
86 if let HookReturn::Handled = hook(id, record, table).await? {
87 return Ok(HookReturn::Handled);
88 }
89 }
90 Ok(HookReturn::Proceed)
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use crate::mocks::mock_table_source::MockTableSource;
97 use crate::table::{AfterFn, BeforeFn, Hook};
98 use serde_json::{Value, json};
99 use std::sync::Arc;
100 use std::sync::atomic::{AtomicUsize, Ordering};
101 use vantage_dataset::prelude::{InsertableValueSet, ReadableValueSet, WritableValueSet};
102
103 type MockTable = Table<MockTableSource, EmptyEntity>;
104
105 fn stamp<'a>(
108 rec: &'a mut Record<Value>,
109 _t: &'a MockTable,
110 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
111 Box::pin(async move {
112 rec.insert("created".into(), json!("yes"));
113 Ok(())
114 })
115 }
116
117 #[allow(clippy::ptr_arg)]
119 fn veto<'a>(
120 _id: &'a String,
121 _rec: &'a Record<Value>,
122 _t: &'a MockTable,
123 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<HookReturn>> + Send + 'a>> {
124 Box::pin(async move { Err(vantage_core::error!("deletion not allowed")) })
125 }
126
127 #[allow(clippy::ptr_arg)]
128 fn soft_delete<'a>(
129 id: &'a String,
130 _rec: &'a Record<Value>,
131 table: &'a MockTable,
132 ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<HookReturn>> + Send + 'a>> {
133 Box::pin(async move {
134 let mut patch = Record::new();
135 patch.insert("deleted".into(), json!("2026-01-01"));
136 table.patch_value(id.clone(), &patch).await?;
137 Ok(HookReturn::Handled)
138 })
139 }
140
141 #[tokio::test]
142 async fn before_insert_mutates_record() {
143 let src = MockTableSource::new().with_data("t", vec![]).await;
144 let table = MockTable::new("t", src)
145 .with_hook(Hook::BeforeInsert(Phase::Populate, Arc::new(stamp)));
146 let id = table
147 .insert_return_id_value(&Record::from(json!({"name": "a"})))
148 .await
149 .unwrap();
150 assert_eq!(
151 table.get_value(id).await.unwrap().unwrap()["created"],
152 json!("yes")
153 );
154 }
155
156 #[tokio::test]
157 async fn before_delete_vetoes() {
158 let src = MockTableSource::new()
159 .with_data("t", vec![json!({"id": "1", "name": "a"})])
160 .await;
161 let table = MockTable::new("t", src).with_hook(Hook::BeforeDelete(Arc::new(veto)));
162 assert!(table.delete("1").await.is_err());
163 assert!(table.get_value("1".to_string()).await.unwrap().is_some());
164 }
165
166 #[tokio::test]
167 async fn before_delete_handled_soft_deletes() {
168 let src = MockTableSource::new()
169 .with_data("t", vec![json!({"id": "1", "name": "a"})])
170 .await;
171 let table = MockTable::new("t", src).with_hook(Hook::BeforeDelete(Arc::new(soft_delete)));
172 table.delete("1").await.unwrap();
173 let row = table.get_value("1".to_string()).await.unwrap().unwrap();
175 assert_eq!(row["deleted"], json!("2026-01-01"));
176 }
177
178 #[tokio::test]
179 async fn after_delete_runs_side_effect() {
180 let hits = Arc::new(AtomicUsize::new(0));
181 let seen = hits.clone();
182 let hook: AfterFn<MockTableSource> = Arc::new(
183 move |_id: &String,
184 _rec: &Record<Value>,
185 _t: &MockTable|
186 -> std::pin::Pin<
187 Box<dyn std::future::Future<Output = Result<()>> + Send + '_>,
188 > {
189 let seen = seen.clone();
190 Box::pin(async move {
191 seen.fetch_add(1, Ordering::SeqCst);
192 Ok(())
193 })
194 },
195 );
196 let src = MockTableSource::new()
197 .with_data("t", vec![json!({"id": "1", "name": "a"})])
198 .await;
199 let table = MockTable::new("t", src).with_hook(Hook::AfterDelete(hook));
200 table.delete("1").await.unwrap();
201 assert_eq!(hits.load(Ordering::SeqCst), 1);
202 }
203
204 #[tokio::test]
205 async fn before_hooks_run_in_phase_order() {
206 let order = Arc::new(std::sync::Mutex::new(Vec::<&'static str>::new()));
207 let recorder = |label: &'static str,
208 log: Arc<std::sync::Mutex<Vec<&'static str>>>|
209 -> BeforeFn<MockTableSource> {
210 Arc::new(
211 move |_r: &mut Record<Value>,
212 _t: &MockTable|
213 -> std::pin::Pin<
214 Box<dyn std::future::Future<Output = Result<()>> + Send + '_>,
215 > {
216 let log = log.clone();
217 Box::pin(async move {
218 log.lock().unwrap().push(label);
219 Ok(())
220 })
221 },
222 )
223 };
224 let src = MockTableSource::new().with_data("t", vec![]).await;
225 let table = MockTable::new("t", src)
227 .with_hook(Hook::BeforeInsert(
228 Phase::Validate,
229 recorder("validate", order.clone()),
230 ))
231 .with_hook(Hook::BeforeInsert(
232 Phase::Normalize,
233 recorder("normalize", order.clone()),
234 ));
235 table
236 .insert_return_id_value(&Record::from(json!({"name": "a"})))
237 .await
238 .unwrap();
239 assert_eq!(*order.lock().unwrap(), vec!["normalize", "validate"]);
240 }
241}