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}