Skip to main content

vantage_table/table/
hooks.rs

1//! Lifecycle hooks attached to a [`Table`].
2//!
3//! Register them with [`Table::with_hook`] / [`Table::add_hook`]. Before-write
4//! hooks run ahead of set-invariant enforcement, ordered by [`Phase`]; the
5//! firing itself lives in the `table::sets` write paths.
6
7use std::future::Future;
8use std::pin::Pin;
9use std::sync::Arc;
10
11use vantage_types::{EmptyEntity, Entity, Record};
12
13use crate::table::Table;
14use crate::traits::table_source::TableSource;
15
16/// Ordering band for before-write hooks. Hooks run in this order (and in
17/// registration order within a band), ahead of set-invariant enforcement.
18#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
19pub enum Phase {
20    /// Clean inputs (trim, default empties).
21    Normalize,
22    /// Set or compute fields (audit stamps, derived values). The default.
23    Populate,
24    /// Read-only checks, last — so they see the final record.
25    Validate,
26}
27
28/// What a `before_delete` hook decided.
29pub enum HookReturn {
30    /// Carry out the underlying operation.
31    Proceed,
32    /// The hook already performed the operation (e.g. a soft-delete patch); skip
33    /// the real one and report success.
34    Handled,
35}
36
37/// A before-write hook: mutate the record in place ahead of invariant
38/// enforcement, returning `Err` to cancel the write. Receives the record being
39/// written and the (entity-erased) table for relation/datasource access.
40pub type BeforeFn<T> = Arc<
41    dyn for<'r> Fn(
42            &'r mut Record<<T as TableSource>::Value>,
43            &'r Table<T, EmptyEntity>,
44        ) -> Pin<Box<dyn Future<Output = vantage_core::Result<()>> + Send + 'r>>
45        + Send
46        + Sync,
47>;
48
49/// A before-delete hook: receives the row id and its current contents; may
50/// `Err` to veto, or return [`HookReturn::Handled`] to take over (soft-delete).
51pub type BeforeDeleteFn<T> = Arc<
52    dyn for<'r> Fn(
53            &'r <T as TableSource>::Id,
54            &'r Record<<T as TableSource>::Value>,
55            &'r Table<T, EmptyEntity>,
56        )
57            -> Pin<Box<dyn Future<Output = vantage_core::Result<HookReturn>> + Send + 'r>>
58        + Send
59        + Sync,
60>;
61
62/// An after-commit hook: side-effects on the committed row (id + record). Used
63/// for inserts, updates, and deletes (where the record is the former contents).
64pub type AfterFn<T> = Arc<
65    dyn for<'r> Fn(
66            &'r <T as TableSource>::Id,
67            &'r Record<<T as TableSource>::Value>,
68            &'r Table<T, EmptyEntity>,
69        ) -> Pin<Box<dyn Future<Output = vantage_core::Result<()>> + Send + 'r>>
70        + Send
71        + Sync,
72>;
73
74/// A lifecycle hook attached to a table via [`Table::with_hook`]. Each variant
75/// carries a closure receiving exactly the references available at that stage.
76/// `BeforeSave`/`AfterSave` are sugar that register for both insert and update.
77pub enum Hook<T: TableSource> {
78    BeforeInsert(Phase, BeforeFn<T>),
79    BeforeUpdate(Phase, BeforeFn<T>),
80    BeforeSave(Phase, BeforeFn<T>),
81    BeforeDelete(BeforeDeleteFn<T>),
82    AfterInsert(AfterFn<T>),
83    AfterUpdate(AfterFn<T>),
84    AfterSave(AfterFn<T>),
85    AfterDelete(AfterFn<T>),
86}
87
88/// A table's registered lifecycle hooks, split by placement. The before-write
89/// bands are kept ordered by [`Phase`]. Populated via [`Table::with_hook`].
90#[derive(Clone)]
91pub struct Hooks<T: TableSource> {
92    pub(crate) before_insert: Vec<(Phase, BeforeFn<T>)>,
93    pub(crate) before_update: Vec<(Phase, BeforeFn<T>)>,
94    pub(crate) before_delete: Vec<BeforeDeleteFn<T>>,
95    pub(crate) after_insert: Vec<AfterFn<T>>,
96    pub(crate) after_update: Vec<AfterFn<T>>,
97    pub(crate) after_delete: Vec<AfterFn<T>>,
98}
99
100impl<T: TableSource> Default for Hooks<T> {
101    fn default() -> Self {
102        Self {
103            before_insert: Vec::new(),
104            before_update: Vec::new(),
105            before_delete: Vec::new(),
106            after_insert: Vec::new(),
107            after_update: Vec::new(),
108            after_delete: Vec::new(),
109        }
110    }
111}
112
113impl<T: TableSource, E: Entity<T::Value>> Table<T, E> {
114    pub(crate) fn before_insert_hooks(&self) -> &[(Phase, BeforeFn<T>)] {
115        &self.hooks.before_insert
116    }
117    pub(crate) fn before_update_hooks(&self) -> &[(Phase, BeforeFn<T>)] {
118        &self.hooks.before_update
119    }
120    pub(crate) fn before_delete_hooks(&self) -> &[BeforeDeleteFn<T>] {
121        &self.hooks.before_delete
122    }
123    pub(crate) fn after_insert_hooks(&self) -> &[AfterFn<T>] {
124        &self.hooks.after_insert
125    }
126    pub(crate) fn after_update_hooks(&self) -> &[AfterFn<T>] {
127        &self.hooks.after_update
128    }
129    pub(crate) fn after_delete_hooks(&self) -> &[AfterFn<T>] {
130        &self.hooks.after_delete
131    }
132
133    /// Register a lifecycle [`Hook`]. Before-write hooks are kept ordered by
134    /// [`Phase`] (then registration order); `BeforeSave`/`AfterSave` register
135    /// for both insert and update.
136    pub fn add_hook(&mut self, hook: Hook<T>) {
137        let hooks = &mut self.hooks;
138        match hook {
139            Hook::BeforeInsert(phase, f) => push_phased(&mut hooks.before_insert, phase, f),
140            Hook::BeforeUpdate(phase, f) => push_phased(&mut hooks.before_update, phase, f),
141            Hook::BeforeSave(phase, f) => {
142                push_phased(&mut hooks.before_insert, phase, f.clone());
143                push_phased(&mut hooks.before_update, phase, f);
144            }
145            Hook::BeforeDelete(f) => hooks.before_delete.push(f),
146            Hook::AfterInsert(f) => hooks.after_insert.push(f),
147            Hook::AfterUpdate(f) => hooks.after_update.push(f),
148            Hook::AfterSave(f) => {
149                hooks.after_insert.push(f.clone());
150                hooks.after_update.push(f);
151            }
152            Hook::AfterDelete(f) => hooks.after_delete.push(f),
153        }
154    }
155
156    /// Builder form of [`Self::add_hook`].
157    pub fn with_hook(mut self, hook: Hook<T>) -> Self {
158        self.add_hook(hook);
159        self
160    }
161}
162
163/// Append a before-write hook and keep the vector ordered by phase (stable, so
164/// registration order is preserved within a phase).
165fn push_phased<T: TableSource>(
166    hooks: &mut Vec<(Phase, BeforeFn<T>)>,
167    phase: Phase,
168    f: BeforeFn<T>,
169) {
170    hooks.push((phase, f));
171    hooks.sort_by_key(|(p, _)| *p);
172}