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