1use async_trait::async_trait;
4use ciborium::Value as CborValue;
5use indexmap::IndexMap;
6use std::sync::{Arc, Mutex};
7use vantage_core::Result;
8use vantage_types::Record;
9
10use crate::{
11 capabilities::VistaCapabilities, column::Column, metadata::VistaMetadata, reference::Reference,
12 sort::SortDirection, source::TableShell, vista::Vista,
13};
14
15#[derive(Clone)]
16pub struct MockShell {
17 data: Arc<Mutex<IndexMap<String, Record<CborValue>>>>,
18 next_auto_id: Arc<Mutex<i64>>,
19 filters: Arc<Mutex<Vec<(String, CborValue)>>>,
20 order: Arc<Mutex<Option<(String, SortDirection)>>>,
21 search: Arc<Mutex<Option<String>>>,
22 capabilities: VistaCapabilities,
23 metadata: VistaMetadata,
24}
25
26impl MockShell {
27 pub fn new() -> Self {
28 Self {
29 data: Arc::new(Mutex::new(IndexMap::new())),
30 next_auto_id: Arc::new(Mutex::new(1)),
31 filters: Arc::new(Mutex::new(Vec::new())),
32 order: Arc::new(Mutex::new(None)),
33 search: Arc::new(Mutex::new(None)),
34 capabilities: VistaCapabilities {
35 can_count: true,
36 can_insert: true,
37 can_update: true,
38 can_delete: true,
39 can_order: true,
40 can_search: true,
41 ..VistaCapabilities::default()
42 },
43 metadata: VistaMetadata::new(),
44 }
45 }
46
47 pub fn with_capabilities(mut self, capabilities: VistaCapabilities) -> Self {
48 self.capabilities = capabilities;
49 self
50 }
51
52 pub fn with_metadata(mut self, metadata: VistaMetadata) -> Self {
53 self.metadata = metadata;
54 self
55 }
56
57 pub fn with_record(self, id: impl Into<String>, record: Record<CborValue>) -> Self {
59 self.data.lock().unwrap().insert(id.into(), record);
60 self
61 }
62
63 fn matches_filters(&self, record: &Record<CborValue>) -> bool {
64 self.filters
65 .lock()
66 .unwrap()
67 .iter()
68 .all(|(field, expected)| record.get(field) == Some(expected))
69 }
70
71 fn matches_search(&self, record: &Record<CborValue>) -> bool {
72 let guard = self.search.lock().unwrap();
73 let Some(needle) = guard.as_deref() else {
74 return true;
75 };
76 let needle_lc = needle.to_lowercase();
77 record.values().any(|v| match v {
78 CborValue::Text(s) => s.to_lowercase().contains(&needle_lc),
79 _ => false,
80 })
81 }
82
83 fn next_auto_id(&self) -> String {
84 let mut next = self.next_auto_id.lock().unwrap();
85 let id = next.to_string();
86 *next += 1;
87 id
88 }
89}
90
91impl Default for MockShell {
92 fn default() -> Self {
93 Self::new()
94 }
95}
96
97#[async_trait]
98impl TableShell for MockShell {
99 fn columns(&self) -> &IndexMap<String, Column> {
100 &self.metadata.columns
101 }
102
103 fn references(&self) -> &IndexMap<String, Reference> {
104 &self.metadata.references
105 }
106
107 fn id_column(&self) -> Option<&str> {
108 self.metadata.id_column.as_deref()
109 }
110
111 async fn list_vista_values(
112 &self,
113 _vista: &Vista,
114 ) -> Result<IndexMap<String, Record<CborValue>>> {
115 let data = self.data.lock().unwrap();
116 let mut rows: Vec<(String, Record<CborValue>)> = data
117 .iter()
118 .filter(|(_, record)| self.matches_filters(record) && self.matches_search(record))
119 .map(|(k, v)| (k.clone(), v.clone()))
120 .collect();
121 if let Some((field, dir)) = self.order.lock().unwrap().clone() {
122 rows.sort_by(|a, b| {
123 let lhs = a.1.get(&field);
124 let rhs = b.1.get(&field);
125 let ord = cbor_cmp(lhs, rhs);
126 match dir {
127 SortDirection::Ascending => ord,
128 SortDirection::Descending => ord.reverse(),
129 }
130 });
131 }
132 Ok(rows.into_iter().collect())
133 }
134
135 async fn get_vista_value(
136 &self,
137 _vista: &Vista,
138 id: &String,
139 ) -> Result<Option<Record<CborValue>>> {
140 Ok(self.data.lock().unwrap().get(id).cloned())
141 }
142
143 async fn get_vista_some_value(
144 &self,
145 _vista: &Vista,
146 ) -> Result<Option<(String, Record<CborValue>)>> {
147 let data = self.data.lock().unwrap();
148 Ok(data
149 .iter()
150 .find(|(_, record)| self.matches_filters(record))
151 .map(|(k, v)| (k.clone(), v.clone())))
152 }
153
154 async fn insert_vista_value(
155 &self,
156 _vista: &Vista,
157 id: &String,
158 record: &Record<CborValue>,
159 ) -> Result<Record<CborValue>> {
160 let mut data = self.data.lock().unwrap();
161 if data.contains_key(id) {
162 return Err(vantage_core::error!("Record already exists", id = id));
163 }
164 let mut stored = record.clone();
165 stored.insert("id".to_string(), CborValue::Text(id.clone()));
166 data.insert(id.clone(), stored.clone());
167 Ok(stored)
168 }
169
170 async fn replace_vista_value(
171 &self,
172 _vista: &Vista,
173 id: &String,
174 record: &Record<CborValue>,
175 ) -> Result<Record<CborValue>> {
176 let mut data = self.data.lock().unwrap();
177 let mut stored = record.clone();
178 stored.insert("id".to_string(), CborValue::Text(id.clone()));
179 data.insert(id.clone(), stored.clone());
180 Ok(stored)
181 }
182
183 async fn patch_vista_value(
184 &self,
185 _vista: &Vista,
186 id: &String,
187 partial: &Record<CborValue>,
188 ) -> Result<Record<CborValue>> {
189 let mut data = self.data.lock().unwrap();
190 let existing = data
191 .get_mut(id)
192 .ok_or_else(|| vantage_core::error!("Record not found", id = id))?;
193 for (k, v) in partial {
194 existing.insert(k.clone(), v.clone());
195 }
196 Ok(existing.clone())
197 }
198
199 async fn delete_vista_value(&self, _vista: &Vista, id: &String) -> Result<()> {
200 let mut data = self.data.lock().unwrap();
201 if data.shift_remove(id).is_none() {
202 Err(vantage_core::error!("Record not found", id = id))
203 } else {
204 Ok(())
205 }
206 }
207
208 async fn delete_vista_all_values(&self, _vista: &Vista) -> Result<()> {
209 self.data.lock().unwrap().clear();
210 Ok(())
211 }
212
213 async fn insert_vista_return_id_value(
214 &self,
215 vista: &Vista,
216 record: &Record<CborValue>,
217 ) -> Result<String> {
218 let id = match record.get("id") {
219 Some(CborValue::Text(s)) if !s.is_empty() => s.clone(),
220 Some(CborValue::Integer(i)) => i128::from(*i).to_string(),
221 _ => self.next_auto_id(),
222 };
223 self.insert_vista_value(vista, &id, record).await?;
224 Ok(id)
225 }
226
227 async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
228 Ok(self.list_vista_values(vista).await?.len() as i64)
229 }
230
231 fn capabilities(&self) -> &VistaCapabilities {
232 &self.capabilities
233 }
234
235 fn driver_name(&self) -> &'static str {
236 "mock"
237 }
238
239 fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
240 self.filters
241 .lock()
242 .unwrap()
243 .push((field.to_string(), value.clone()));
244 Ok(())
245 }
246
247 fn add_order(&mut self, field: &str, dir: SortDirection) -> Result<()> {
248 *self.order.lock().unwrap() = Some((field.to_string(), dir));
249 Ok(())
250 }
251
252 fn clear_orders(&mut self) -> Result<()> {
253 *self.order.lock().unwrap() = None;
254 Ok(())
255 }
256
257 fn add_search(&mut self, text: &str) -> Result<()> {
258 *self.search.lock().unwrap() = Some(text.to_string());
259 Ok(())
260 }
261
262 fn clear_search(&mut self) -> Result<()> {
263 *self.search.lock().unwrap() = None;
264 Ok(())
265 }
266}
267
268fn cbor_cmp(a: Option<&CborValue>, b: Option<&CborValue>) -> std::cmp::Ordering {
273 use std::cmp::Ordering;
274 match (a, b) {
275 (None, None) => Ordering::Equal,
276 (None, _) => Ordering::Less,
277 (_, None) => Ordering::Greater,
278 (Some(lhs), Some(rhs)) => match (lhs, rhs) {
279 (CborValue::Text(l), CborValue::Text(r)) => l.cmp(r),
280 (CborValue::Integer(l), CborValue::Integer(r)) => i128::from(*l).cmp(&i128::from(*r)),
281 (CborValue::Bool(l), CborValue::Bool(r)) => l.cmp(r),
282 _ => format!("{lhs:?}").cmp(&format!("{rhs:?}")),
283 },
284 }
285}
286
287#[cfg(test)]
288mod tests {
289 use super::*;
290 use crate::{Column, Reference, ReferenceKind, Vista, VistaMetadata};
291 use vantage_dataset::{InsertableValueSet, ReadableValueSet, WritableValueSet};
292
293 fn cbor_text(s: &str) -> CborValue {
294 CborValue::Text(s.into())
295 }
296
297 fn record(pairs: &[(&str, CborValue)]) -> Record<CborValue> {
298 let mut r = Record::new();
299 for (k, v) in pairs {
300 r.insert((*k).to_string(), v.clone());
301 }
302 r
303 }
304
305 fn build_user_vista(source: MockShell) -> Vista {
306 let metadata = VistaMetadata::new()
307 .with_column(Column::new("id", "String").with_flag("id"))
308 .with_column(Column::new("name", "String").with_flag("title"))
309 .with_column(Column::new("email", "String").hidden())
310 .with_column(Column::new("vip_flag", "bool"))
311 .with_id_column("id")
312 .with_reference(Reference::new(
313 "orders",
314 "orders",
315 ReferenceKind::HasMany,
316 "user_id",
317 ));
318 Vista::new("users", Box::new(source.with_metadata(metadata)))
319 }
320
321 #[test]
322 fn metadata_accessors_round_trip() {
323 let vista = build_user_vista(MockShell::new());
324
325 assert_eq!(vista.name(), "users");
326 assert_eq!(vista.get_id_column(), Some("id"));
327 assert_eq!(vista.get_title_columns(), vec!["name"]);
328 assert_eq!(
329 vista.get_column_names(),
330 vec!["id", "name", "email", "vip_flag"]
331 );
332 assert!(vista.get_column("email").unwrap().is_hidden());
333 assert!(!vista.get_column("name").unwrap().is_hidden());
334 assert_eq!(vista.get_references(), vec!["orders".to_string()]);
335 assert_eq!(
336 vista.get_reference("orders").unwrap().foreign_key,
337 "user_id"
338 );
339
340 let caps = vista.capabilities();
341 assert!(caps.can_count && caps.can_insert && caps.can_update && caps.can_delete);
342 assert!(!caps.can_subscribe);
343 }
344
345 #[tokio::test]
346 async fn list_values_returns_seeded_rows() {
347 let source = MockShell::new()
348 .with_record(
349 "1",
350 record(&[("id", cbor_text("1")), ("name", cbor_text("Alice"))]),
351 )
352 .with_record(
353 "2",
354 record(&[("id", cbor_text("2")), ("name", cbor_text("Bob"))]),
355 );
356 let vista = build_user_vista(source);
357
358 let rows = vista.list_values().await.unwrap();
359 assert_eq!(rows.len(), 2);
360 assert!(rows.contains_key("1"));
361 assert_eq!(rows["2"].get("name"), Some(&cbor_text("Bob")));
362
363 let alice = vista.get_value(&"1".to_string()).await.unwrap().unwrap();
364 assert_eq!(alice.get("name"), Some(&cbor_text("Alice")));
365
366 assert_eq!(vista.get_count().await.unwrap(), 2);
367 }
368
369 #[tokio::test]
370 async fn add_condition_eq_filters_list_and_count() {
371 let source = MockShell::new()
372 .with_record(
373 "1",
374 record(&[
375 ("name", cbor_text("Alice")),
376 ("vip_flag", CborValue::Bool(true)),
377 ]),
378 )
379 .with_record(
380 "2",
381 record(&[
382 ("name", cbor_text("Bob")),
383 ("vip_flag", CborValue::Bool(false)),
384 ]),
385 )
386 .with_record(
387 "3",
388 record(&[
389 ("name", cbor_text("Carol")),
390 ("vip_flag", CborValue::Bool(true)),
391 ]),
392 );
393 let mut vista = build_user_vista(source);
394 vista
395 .add_condition_eq("vip_flag", CborValue::Bool(true))
396 .unwrap();
397
398 let rows = vista.list_values().await.unwrap();
399 assert_eq!(rows.len(), 2);
400 assert!(rows.contains_key("1"));
401 assert!(rows.contains_key("3"));
402 assert_eq!(vista.get_count().await.unwrap(), 2);
403 }
404
405 #[tokio::test]
406 async fn writable_value_set_round_trip() {
407 let vista = build_user_vista(MockShell::new());
408
409 let inserted = vista
411 .insert_value(
412 &"alice".to_string(),
413 &record(&[("name", cbor_text("Alice"))]),
414 )
415 .await
416 .unwrap();
417 assert_eq!(inserted.get("id"), Some(&cbor_text("alice")));
418
419 let dup = vista.insert_value(&"alice".to_string(), &record(&[])).await;
421 assert!(dup.is_err());
422
423 vista
425 .replace_value(
426 &"alice".to_string(),
427 &record(&[("name", cbor_text("Alicia"))]),
428 )
429 .await
430 .unwrap();
431 let renamed = vista
432 .get_value(&"alice".to_string())
433 .await
434 .unwrap()
435 .unwrap();
436 assert_eq!(renamed.get("name"), Some(&cbor_text("Alicia")));
437
438 vista
440 .patch_value(
441 &"alice".to_string(),
442 &record(&[("email", cbor_text("alice@example.com"))]),
443 )
444 .await
445 .unwrap();
446 let patched = vista
447 .get_value(&"alice".to_string())
448 .await
449 .unwrap()
450 .unwrap();
451 assert_eq!(patched.get("name"), Some(&cbor_text("Alicia")));
452 assert_eq!(patched.get("email"), Some(&cbor_text("alice@example.com")));
453
454 vista.delete(&"alice".to_string()).await.unwrap();
456 assert!(
457 vista
458 .get_value(&"alice".to_string())
459 .await
460 .unwrap()
461 .is_none()
462 );
463
464 vista
466 .insert_value(&"a".to_string(), &record(&[("name", cbor_text("A"))]))
467 .await
468 .unwrap();
469 vista
470 .insert_value(&"b".to_string(), &record(&[("name", cbor_text("B"))]))
471 .await
472 .unwrap();
473 vista.delete_all().await.unwrap();
474 assert_eq!(vista.list_values().await.unwrap().len(), 0);
475 }
476
477 #[tokio::test]
478 async fn insertable_value_set_assigns_ids() {
479 let vista = build_user_vista(MockShell::new());
480
481 let auto_id = vista
483 .insert_return_id_value(&record(&[("name", cbor_text("Bob"))]))
484 .await
485 .unwrap();
486 assert_eq!(auto_id, "1");
487
488 let explicit = vista
490 .insert_return_id_value(&record(&[
491 ("id", cbor_text("alice")),
492 ("name", cbor_text("Alice")),
493 ]))
494 .await
495 .unwrap();
496 assert_eq!(explicit, "alice");
497
498 assert_eq!(vista.get_count().await.unwrap(), 2);
499 }
500}