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