1use indexmap::IndexMap;
10use serde::{Deserialize, Serialize, de::DeserializeOwned};
11
12use crate::reference::ReferenceKind;
13
14#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(deny_unknown_fields)]
17pub struct NoExtras {}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
27#[serde(bound(
28 serialize = "T: Serialize, C: Serialize, R: Serialize",
29 deserialize = "T: DeserializeOwned + Default, C: DeserializeOwned + Default, R: DeserializeOwned + Default"
30))]
31pub struct VistaSpec<T = NoExtras, C = NoExtras, R = NoExtras> {
32 pub name: String,
33 #[serde(default, skip_serializing_if = "Option::is_none")]
34 pub datasource: Option<String>,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub id_column: Option<String>,
37 pub columns: IndexMap<String, ColumnSpec<C>>,
38 #[serde(default, skip_serializing_if = "IndexMap::is_empty")]
39 pub references: IndexMap<String, ReferenceSpec<R>>,
40 #[serde(flatten, default)]
41 pub driver: T,
42}
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
52#[serde(bound(
53 serialize = "C: Serialize",
54 deserialize = "C: DeserializeOwned + Default"
55))]
56pub struct ColumnSpec<C = NoExtras> {
57 #[serde(rename = "type", default, skip_serializing_if = "Option::is_none")]
58 pub col_type: Option<String>,
59 #[serde(default, skip_serializing_if = "Vec::is_empty")]
60 pub flags: Vec<String>,
61 #[serde(default, skip_serializing_if = "Option::is_none")]
62 pub references: Option<ReferenceSugar>,
63 #[serde(flatten, default)]
64 pub driver: C,
65}
66
67impl<C: Default> ColumnSpec<C> {
68 pub fn new() -> Self {
69 Self {
70 col_type: None,
71 flags: Vec::new(),
72 references: None,
73 driver: C::default(),
74 }
75 }
76
77 pub fn with_type(mut self, ty: impl Into<String>) -> Self {
78 self.col_type = Some(ty.into());
79 self
80 }
81
82 pub fn with_flag(mut self, flag: impl Into<String>) -> Self {
83 self.flags.push(flag.into());
84 self
85 }
86}
87
88impl<C: Default> Default for ColumnSpec<C> {
89 fn default() -> Self {
90 Self::new()
91 }
92}
93
94#[derive(Debug, Clone, Serialize, Deserialize)]
96#[serde(bound(
97 serialize = "R: Serialize",
98 deserialize = "R: DeserializeOwned + Default"
99))]
100pub struct ReferenceSpec<R = NoExtras> {
101 pub table: String,
102 #[serde(default)]
103 pub kind: ReferenceKind,
104 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub foreign_key: Option<String>,
106 #[serde(flatten, default)]
107 pub driver: R,
108}
109
110#[derive(Debug, Clone, Serialize, Deserialize)]
116#[serde(untagged)]
117pub enum ReferenceSugar {
118 Sugar(String),
119 Full(ReferenceSpec<NoExtras>),
120}
121
122#[cfg(test)]
123mod tests {
124 use super::*;
125 use serde::{Deserialize, Serialize};
126
127 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
128 #[serde(deny_unknown_fields)]
129 struct DummyTable {
130 dummy: DummyTableBlock,
131 }
132
133 #[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
134 #[serde(deny_unknown_fields)]
135 struct DummyTableBlock {
136 path: String,
137 }
138
139 type DummySpec = VistaSpec<DummyTable, NoExtras, NoExtras>;
140
141 #[test]
142 fn parses_minimal_spec() {
143 let yaml = r#"
144name: clients
145columns:
146 id:
147 type: int
148 flags: [id]
149 name:
150 type: string
151 flags: [title, searchable]
152dummy:
153 path: data/clients.csv
154"#;
155 let spec: DummySpec = serde_yaml_ng::from_str(yaml).unwrap();
156 assert_eq!(spec.name, "clients");
157 assert_eq!(spec.columns.len(), 2);
158 assert_eq!(spec.columns["id"].col_type.as_deref(), Some("int"));
159 assert_eq!(spec.columns["name"].flags, vec!["title", "searchable"]);
160 assert_eq!(spec.driver.dummy.path, "data/clients.csv");
161 }
162
163 #[test]
164 fn rejects_unknown_driver_field() {
165 let yaml = r#"
166name: clients
167columns:
168 id: { type: int }
169dummy:
170 path: x
171 bogus: 1
172"#;
173 let err = serde_yaml_ng::from_str::<DummySpec>(yaml).unwrap_err();
174 assert!(
175 err.to_string().contains("bogus") || err.to_string().contains("unknown"),
176 "expected typo-detecting error, got: {err}"
177 );
178 }
179
180 #[test]
181 fn reference_sugar_round_trip() {
182 let yaml = r#"
183name: clients
184columns:
185 shop_id:
186 type: int
187 references: shops
188dummy:
189 path: x
190"#;
191 let spec: DummySpec = serde_yaml_ng::from_str(yaml).unwrap();
192 match spec.columns["shop_id"].references.as_ref().unwrap() {
193 ReferenceSugar::Sugar(s) => assert_eq!(s, "shops"),
194 other => panic!("expected sugar, got {other:?}"),
195 }
196 }
197
198 #[test]
199 fn full_reference_form() {
200 let yaml = r#"
201name: orders
202columns:
203 user_id:
204 type: int
205 references:
206 table: users
207 kind: has_one
208 foreign_key: user_id
209dummy:
210 path: x
211"#;
212 let spec: DummySpec = serde_yaml_ng::from_str(yaml).unwrap();
213 match spec.columns["user_id"].references.as_ref().unwrap() {
214 ReferenceSugar::Full(r) => {
215 assert_eq!(r.table, "users");
216 assert_eq!(r.kind, ReferenceKind::HasOne);
217 assert_eq!(r.foreign_key.as_deref(), Some("user_id"));
218 }
219 other => panic!("expected full, got {other:?}"),
220 }
221 }
222}