Skip to main content

vantage_vista/
reference.rs

1use serde::{Deserialize, Serialize};
2use vantage_core::Result;
3
4mod join_key;
5mod narrow;
6mod validate;
7
8pub use join_key::JoinKey;
9
10/// A relation a Vista exposes: a join to another table, or records contained
11/// in a column of this table's rows. Traversed with
12/// [`Vista::get_ref`](crate::Vista::get_ref).
13///
14/// A join (`HasOne`, `HasMany`) narrows the `target` table by its `keys`:
15/// each [`JoinKey`] reads the parent row's `from` column and constrains the
16/// target's `to` column to that value. A contained relation (`ContainsOne`,
17/// `ContainsMany`) reads the parent row's `host_column` and has no keys.
18///
19/// Build one with the constructor of its kind, so the kind and its fields
20/// agree. Deserialising checks the same agreement: a contained kind needs a
21/// `host_column` and no keys, a join needs at least one key and no
22/// `host_column`, and only a join takes a `build_script`.
23#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
24#[serde(try_from = "validate::RawReference")]
25#[non_exhaustive]
26pub struct Reference {
27    pub name: String,
28    pub kind: ReferenceKind,
29    /// The target table; for a contained relation, the relation's own name.
30    pub target: String,
31    #[serde(default, skip_serializing_if = "Vec::is_empty")]
32    pub keys: Vec<JoinKey>,
33    /// Contained kinds: the parent column holding the embedded object (one)
34    /// or array of objects (many).
35    #[serde(default, skip_serializing_if = "Option::is_none")]
36    pub host_column: Option<String>,
37    /// Rhai script that builds a join's traversal target in place of the
38    /// keys, evaluated with the parent `row` in scope (`rhai` feature).
39    /// Lowered from the per-reference YAML extras slot by each backend
40    /// factory.
41    #[serde(default, skip_serializing_if = "Option::is_none")]
42    pub build_script: Option<String>,
43}
44
45impl Reference {
46    /// One `target` row, found through `key` on the parent row. A join on
47    /// several keys is lowered from YAML by
48    /// [`ReferenceSpec::to_reference`](crate::ReferenceSpec::to_reference).
49    pub fn has_one(name: impl Into<String>, target: impl Into<String>, key: JoinKey) -> Self {
50        Self::join(name.into(), ReferenceKind::HasOne, target.into(), key)
51    }
52
53    /// The `target` rows whose `key` matches the parent row. A join on
54    /// several keys is lowered from YAML by
55    /// [`ReferenceSpec::to_reference`](crate::ReferenceSpec::to_reference).
56    pub fn has_many(name: impl Into<String>, target: impl Into<String>, key: JoinKey) -> Self {
57        Self::join(name.into(), ReferenceKind::HasMany, target.into(), key)
58    }
59
60    /// One more key a join narrows by; every key must match.
61    pub(crate) fn with_key(mut self, key: JoinKey) -> Self {
62        self.keys.push(key);
63        self
64    }
65
66    /// One record embedded as an object in the parent's `host_column`.
67    pub fn contains_one(name: impl Into<String>, host_column: impl Into<String>) -> Self {
68        Self::contained(name.into(), ReferenceKind::ContainsOne, host_column.into())
69    }
70
71    /// Records embedded as an array in the parent's `host_column`.
72    pub fn contains_many(name: impl Into<String>, host_column: impl Into<String>) -> Self {
73        Self::contained(name.into(), ReferenceKind::ContainsMany, host_column.into())
74    }
75
76    /// Attach a Rhai build script; see [`build_script`](Self::build_script).
77    /// A contained relation has no traversal target to build, so it is a
78    /// plain error naming the relation.
79    pub fn with_build_script(mut self, script: impl Into<String>) -> Result<Self> {
80        validate::check_build_script(&self.name, self.kind)?;
81        self.build_script = Some(script.into());
82        Ok(self)
83    }
84
85    /// The foreign-key column of a single-key join: on the parent row for
86    /// `HasOne` (the key's `from`), on the target for `HasMany` (its `to`).
87    /// `None` for a contained relation or a join with several keys.
88    pub(crate) fn foreign_key(&self) -> Option<&str> {
89        match (self.kind, self.keys.as_slice()) {
90            (ReferenceKind::HasOne, [key]) => Some(&key.from),
91            (ReferenceKind::HasMany, [key]) => key.to.as_deref(),
92            _ => None,
93        }
94    }
95
96    fn join(name: String, kind: ReferenceKind, target: String, key: JoinKey) -> Self {
97        Self {
98            name,
99            kind,
100            target,
101            keys: vec![key],
102            host_column: None,
103            build_script: None,
104        }
105    }
106
107    fn contained(name: String, kind: ReferenceKind, host_column: String) -> Self {
108        Self {
109            target: name.clone(),
110            name,
111            kind,
112            keys: Vec::new(),
113            host_column: Some(host_column),
114            build_script: None,
115        }
116    }
117}
118
119/// What a relation points at, and how many records it yields.
120#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
121#[serde(rename_all = "snake_case")]
122#[non_exhaustive]
123pub enum ReferenceKind {
124    /// One target row, found by a key on the parent row.
125    HasOne,
126    /// Target rows that carry the parent's key.
127    HasMany,
128    /// One record embedded as an object in a parent column.
129    ContainsOne,
130    /// Records embedded as an array in a parent column.
131    ContainsMany,
132}
133
134impl ReferenceKind {
135    /// Whether the relation's records live inside the parent row.
136    pub fn is_contained(self) -> bool {
137        matches!(
138            self,
139            ReferenceKind::ContainsOne | ReferenceKind::ContainsMany
140        )
141    }
142
143    /// The kind as YAML spells it: `has_one`, `has_many`, `contains_one`,
144    /// `contains_many`.
145    pub fn as_str(self) -> &'static str {
146        match self {
147            ReferenceKind::HasOne => "has_one",
148            ReferenceKind::HasMany => "has_many",
149            ReferenceKind::ContainsOne => "contains_one",
150            ReferenceKind::ContainsMany => "contains_many",
151        }
152    }
153}