1use indexmap::IndexMap;
10use serde::{Deserialize, Serialize, de::DeserializeOwned};
11use vantage_core::Result;
12
13use crate::column::{Column, ColumnFlag};
14use crate::reference::{JoinKey, ReferenceKind};
15
16mod lowering;
17pub use lowering::{SpecResolver, resolve_base_spec};
18
19#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
21#[serde(deny_unknown_fields)]
22#[non_exhaustive]
23pub struct NoExtras {}
24
25#[derive(Debug, Clone, Serialize, Deserialize)]
33#[serde(bound(
34 serialize = "T: Serialize, C: Serialize, R: Serialize",
35 deserialize = "T: DeserializeOwned + Default, C: DeserializeOwned + Default, R: DeserializeOwned + Default"
36))]
37#[non_exhaustive]
38pub struct VistaSpec<T = NoExtras, C = NoExtras, R = NoExtras> {
39 pub name: String,
40 #[serde(default, skip_serializing_if = "Option::is_none")]
41 pub datasource: Option<String>,
42 #[serde(default, skip_serializing_if = "Option::is_none")]
43 pub id_column: Option<String>,
44 pub columns: IndexMap<String, ColumnSpec<C>>,
45 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
46 pub references: IndexMap<String, ReferenceSpec<R>>,
47 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
50 pub contained: IndexMap<String, ContainedYaml<C>>,
51 #[serde(flatten, default)]
52 pub driver: T,
53}
54
55impl<T: Default, C, R> VistaSpec<T, C, R> {
56 pub fn new(name: impl Into<String>) -> Self {
59 Self {
60 name: name.into(),
61 datasource: None,
62 id_column: None,
63 columns: IndexMap::new(),
64 references: IndexMap::new(),
65 contained: IndexMap::new(),
66 driver: T::default(),
67 }
68 }
69}
70
71#[derive(Debug, Clone, Serialize, Deserialize)]
76#[serde(
77 deny_unknown_fields,
78 bound(
79 serialize = "C: Serialize",
80 deserialize = "C: DeserializeOwned + Default"
81 )
82)]
83#[non_exhaustive]
84pub struct ContainedYaml<C = NoExtras> {
85 pub host_column: String,
87 #[serde(default = "contains_one")]
89 pub kind: ReferenceKind,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub id_column: Option<String>,
93 pub columns: IndexMap<String, ColumnSpec<C>>,
95}
96
97fn contains_one() -> ReferenceKind {
98 ReferenceKind::ContainsOne
99}
100
101fn has_one() -> ReferenceKind {
102 ReferenceKind::HasOne
103}
104
105#[derive(Debug, Clone, Serialize, Deserialize)]
114#[serde(bound(
115 serialize = "C: Serialize",
116 deserialize = "C: DeserializeOwned + Default"
117))]
118#[non_exhaustive]
119pub struct ColumnSpec<C = NoExtras> {
120 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
121 pub col_type: Option<String>,
122 #[serde(default, skip_serializing_if = "Vec::is_empty")]
123 pub flags: Vec<ColumnFlag>,
124 #[serde(default, skip_serializing_if = "Option::is_none")]
125 pub references: Option<ReferenceSugar>,
126 #[serde(default, skip_serializing_if = "Option::is_none")]
134 pub lazy: Option<String>,
135 #[serde(default, skip_serializing_if = "Option::is_none")]
143 pub expr: Option<String>,
144 #[serde(flatten, default)]
145 pub driver: C,
146}
147
148impl<C: Default> ColumnSpec<C> {
149 pub fn new() -> Self {
150 Self {
151 col_type: None,
152 flags: Vec::new(),
153 references: None,
154 lazy: None,
155 expr: None,
156 driver: C::default(),
157 }
158 }
159
160 pub fn with_type(mut self, ty: impl Into<String>) -> Self {
161 self.col_type = Some(ty.into());
162 self
163 }
164
165 pub fn with_flag(mut self, flag: impl Into<ColumnFlag>) -> Self {
166 self.flags.push(ColumnFlag::from(flag.into().as_str()));
167 self
168 }
169
170 pub fn with_lazy(mut self, script: impl Into<String>) -> Self {
171 self.lazy = Some(script.into());
172 self
173 }
174
175 pub fn with_expr(mut self, script: impl Into<String>) -> Self {
176 self.expr = Some(script.into());
177 self
178 }
179}
180
181impl<C> ColumnSpec<C> {
182 pub fn lazy_column(&self, name: &str) -> Result<Option<Column>> {
187 let Some(code) = &self.lazy else {
188 return Ok(None);
189 };
190 let typed = match &self.col_type {
191 Some(ty) => Column::new(name, ty),
192 None => Column::untyped(name),
193 };
194 let column = self.flags.iter().cloned().fold(typed, Column::with_flag);
195 computed(column, code).map(Some)
196 }
197}
198
199#[cfg(feature = "rhai")]
200fn computed(column: Column, code: &str) -> Result<Column> {
201 column.with_expression(code)
202}
203
204#[cfg(not(feature = "rhai"))]
205fn computed(column: Column, _code: &str) -> Result<Column> {
206 Err(vantage_core::error!(
207 "Column declares a `lazy:` script but vantage-vista was built without the `rhai` feature",
208 column = column.name
209 ))
210}
211
212impl<C: Default> Default for ColumnSpec<C> {
213 fn default() -> Self {
214 Self::new()
215 }
216}
217
218#[derive(Debug, Clone, Serialize, Deserialize)]
220#[serde(bound(
221 serialize = "R: Serialize",
222 deserialize = "R: DeserializeOwned + Default"
223))]
224#[non_exhaustive]
225pub struct ReferenceSpec<R = NoExtras> {
226 pub table: String,
227 #[serde(default = "has_one")]
229 pub kind: ReferenceKind,
230 #[serde(default, skip_serializing_if = "Option::is_none")]
231 pub foreign_key: Option<String>,
232 #[serde(default, skip_serializing_if = "Vec::is_empty")]
239 pub keys: Vec<JoinKey>,
240 #[serde(flatten, default)]
241 pub driver: R,
242}
243
244#[derive(Debug, Clone, Serialize, Deserialize)]
250#[serde(untagged)]
251#[non_exhaustive]
252pub enum ReferenceSugar {
253 Sugar(String),
254 Full(ReferenceSpec<NoExtras>),
255}