Skip to main content

vantage_table/table/
inject.rs

1//! The build injector: a hook a vista factory runs on every table it builds
2//! from a spec — the root, each reference target and each `base:` table —
3//! after the table is built and before it is wrapped. It may add conditions
4//! and register hooks, or fail the build.
5//!
6//! Each backend's spec builder threads a [`BuildEnv`] through its builds and
7//! ends with [`BuildEnv::inject`]; `Table::with_spec_references` hands each
8//! reference target a fresh environment of kind [`BuildKind::Reference`].
9
10use std::fmt;
11use std::sync::Arc;
12
13use vantage_core::{Result, error};
14use vantage_types::EmptyEntity;
15use vantage_vista::SpecResolver;
16
17use crate::table::Table;
18use crate::traits::table_source::TableSource;
19
20/// Why a table is being built.
21#[derive(Clone, Debug, PartialEq, Eq)]
22#[non_exhaustive]
23pub enum BuildKind {
24    /// A table built for its own sake: a factory's `build_from_spec`, or a
25    /// table an injector asked for through [`InjectCx::build`].
26    Root,
27    /// The target of `relation` on the spec named `from`, built when the
28    /// relation is traversed.
29    Reference { from: String, relation: String },
30    /// The `base:` table the spec named `derived` reads from. Only the
31    /// conditions an injector adds here take effect: the derived table is a
32    /// fresh table over the base's `select()`, so hooks registered on a
33    /// `Base` build never fire. Register hooks on the derived build instead.
34    Base { derived: String },
35}
36
37/// What an injector receives besides the table: the spec key being built,
38/// why, and a builder for other tables of the same backend.
39#[non_exhaustive]
40pub struct InjectCx<'a, T: TableSource> {
41    pub key: &'a str,
42    pub kind: BuildKind,
43    /// Build another table by spec key, through the same factory and the
44    /// same injector (its kind is [`BuildKind::Root`]). Errors when the
45    /// factory has no resolver, the key has no spec, or the build would
46    /// re-enter a key already being built (a cycle).
47    pub build: &'a dyn Fn(&str) -> Result<Table<T, EmptyEntity>>,
48}
49
50/// A build injector. Typed per backend, since each backend's table is.
51pub type TableInjector<T> =
52    Arc<dyn Fn(&InjectCx<'_, T>, &mut Table<T, EmptyEntity>) -> Result<()> + Send + Sync>;
53
54/// The spec keys being built, outermost first. Nested builds (an injector
55/// building a parent, a `base:` chain) extend it; re-entering a key is a cycle.
56#[derive(Clone, Debug, Default, PartialEq, Eq)]
57pub struct BuildTrail {
58    keys: Vec<String>,
59}
60
61impl BuildTrail {
62    /// The trail with `key` appended; an error naming the path when `key` is
63    /// already on it.
64    pub fn enter(&self, key: &str) -> Result<BuildTrail> {
65        if self.keys.iter().any(|k| k == key) {
66            let path = self
67                .keys
68                .iter()
69                .map(String::as_str)
70                .chain([key])
71                .collect::<Vec<_>>()
72                .join(" -> ");
73            return Err(error!("table build cycle", path = path));
74        }
75        let mut keys = self.keys.clone();
76        keys.push(key.to_string());
77        Ok(BuildTrail { keys })
78    }
79
80    pub fn keys(&self) -> &[String] {
81        &self.keys
82    }
83}
84
85/// Builds a table from spec `S` over data source `T`, carrying the build
86/// environment into nested builds.
87pub type SpecTableBuilder<S, T> = fn(&S, T, &BuildEnv<S, T>) -> Result<Table<T, EmptyEntity>>;
88
89/// What a spec build carries besides the spec and the data source: the
90/// resolver for references and `base:`, the injector, the trail of keys
91/// being built, and the kind of this build.
92pub struct BuildEnv<S, T: TableSource> {
93    pub resolver: Option<Arc<dyn SpecResolver<S>>>,
94    pub injector: Option<TableInjector<T>>,
95    pub trail: BuildTrail,
96    pub kind: BuildKind,
97}
98
99impl<S, T: TableSource> Clone for BuildEnv<S, T> {
100    fn clone(&self) -> Self {
101        Self {
102            resolver: self.resolver.clone(),
103            injector: self.injector.clone(),
104            trail: self.trail.clone(),
105            kind: self.kind.clone(),
106        }
107    }
108}
109
110impl<S, T: TableSource> fmt::Debug for BuildEnv<S, T> {
111    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
112        f.debug_struct("BuildEnv")
113            .field("resolver", &self.resolver.is_some())
114            .field("injector", &self.injector.is_some())
115            .field("trail", &self.trail)
116            .field("kind", &self.kind)
117            .finish()
118    }
119}
120
121impl<S: 'static, T: TableSource> BuildEnv<S, T> {
122    /// The environment of a root build of `key`.
123    pub fn root(
124        key: &str,
125        resolver: Option<Arc<dyn SpecResolver<S>>>,
126        injector: Option<TableInjector<T>>,
127    ) -> Self {
128        Self {
129            resolver,
130            injector,
131            trail: BuildTrail {
132                keys: vec![key.to_string()],
133            },
134            kind: BuildKind::Root,
135        }
136    }
137
138    /// The environment for building `key` from inside this build, as `kind`.
139    /// Errors when `key` is already being built.
140    pub fn nested(&self, key: &str, kind: BuildKind) -> Result<Self> {
141        Ok(Self {
142            resolver: self.resolver.clone(),
143            injector: self.injector.clone(),
144            trail: self.trail.enter(key)?,
145            kind,
146        })
147    }
148
149    /// Run the injector, if any, on `table`, the result of building `key`.
150    /// `build` is the backend's spec builder, used for the tables the
151    /// injector asks for.
152    pub fn inject(
153        &self,
154        key: &str,
155        table: &mut Table<T, EmptyEntity>,
156        build: SpecTableBuilder<S, T>,
157    ) -> Result<()> {
158        let Some(injector) = &self.injector else {
159            return Ok(());
160        };
161        let db = table.data_source().clone();
162        let nested = |other: &str| -> Result<Table<T, EmptyEntity>> {
163            let env = self.nested(other, BuildKind::Root)?;
164            let resolver = self.resolver.as_ref().ok_or_else(|| {
165                error!(
166                    "the injector's table builder needs a spec resolver",
167                    key = other
168                )
169            })?;
170            let spec = resolver
171                .resolve(other)
172                .ok_or_else(|| error!("the injector's table builder found no spec", key = other))?;
173            build(&spec, db.clone(), &env)
174        };
175        let cx = InjectCx {
176            key,
177            kind: self.kind.clone(),
178            build: &nested,
179        };
180        injector(&cx, table)
181    }
182}