1use async_trait::async_trait;
4use ciborium::Value as CborValue;
5use indexmap::IndexMap;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, Mutex};
8use vantage_core::Result;
9use vantage_types::Record;
10
11use crate::{
12 build_contained_vista,
13 capabilities::VistaCapabilities,
14 column::Column,
15 contained::ContainedWriteback,
16 metadata::VistaMetadata,
17 reference::{ContainedSpec, Reference},
18 sort::SortDirection,
19 source::TableShell,
20 vista::Vista,
21};
22
23#[derive(Clone)]
24pub struct MockShell {
25 data: Arc<Mutex<IndexMap<String, Record<CborValue>>>>,
26 next_auto_id: Arc<Mutex<i64>>,
27 filters: Arc<Mutex<Vec<(String, CborValue)>>>,
28 order: Arc<Mutex<Option<(String, SortDirection)>>>,
29 search: Arc<Mutex<Option<String>>>,
30 capabilities: VistaCapabilities,
31 metadata: VistaMetadata,
32 ref_targets: IndexMap<String, MockShell>,
36 fail_reads: Arc<AtomicBool>,
40}
41
42impl MockShell {
43 pub fn new() -> Self {
44 Self {
45 data: Arc::new(Mutex::new(IndexMap::new())),
46 next_auto_id: Arc::new(Mutex::new(1)),
47 filters: Arc::new(Mutex::new(Vec::new())),
48 order: Arc::new(Mutex::new(None)),
49 search: Arc::new(Mutex::new(None)),
50 capabilities: VistaCapabilities {
51 can_count: true,
52 can_insert: true,
53 can_update: true,
54 can_delete: true,
55 can_order: true,
56 can_search: true,
57 ..VistaCapabilities::default()
58 },
59 metadata: VistaMetadata::new(),
60 ref_targets: IndexMap::new(),
61 fail_reads: Arc::new(AtomicBool::new(false)),
62 }
63 }
64
65 pub fn with_ref_target(mut self, relation: impl Into<String>, target: MockShell) -> Self {
70 self.ref_targets.insert(relation.into(), target);
71 self
72 }
73
74 pub fn set_fail_reads(&self, fail: bool) {
79 self.fail_reads.store(fail, Ordering::SeqCst);
80 }
81
82 fn narrowed_clone(&self) -> Self {
86 Self {
87 data: self.data.clone(),
88 next_auto_id: self.next_auto_id.clone(),
89 filters: Arc::new(Mutex::new(Vec::new())),
90 order: Arc::new(Mutex::new(None)),
91 search: Arc::new(Mutex::new(None)),
92 capabilities: self.capabilities.clone(),
93 metadata: self.metadata.clone(),
94 ref_targets: self.ref_targets.clone(),
95 fail_reads: self.fail_reads.clone(),
96 }
97 }
98
99 pub fn with_capabilities(mut self, capabilities: VistaCapabilities) -> Self {
100 self.capabilities = capabilities;
101 self
102 }
103
104 pub fn with_metadata(mut self, metadata: VistaMetadata) -> Self {
105 self.metadata = metadata;
106 self
107 }
108
109 pub fn with_record(self, id: impl Into<String>, record: Record<CborValue>) -> Self {
111 self.data.lock().unwrap().insert(id.into(), record);
112 self
113 }
114
115 pub fn set_record(&self, id: impl Into<String>, record: Record<CborValue>) {
126 self.data.lock().unwrap().insert(id.into(), record);
127 }
128
129 pub fn set_field(&self, id: &str, field: &str, value: CborValue) {
132 if let Some(rec) = self.data.lock().unwrap().get_mut(id) {
133 rec.insert(field.to_string(), value);
134 }
135 }
136
137 pub fn remove_record(&self, id: &str) {
139 self.data.lock().unwrap().shift_remove(id);
140 }
141
142 pub fn clear_records(&self) {
144 self.data.lock().unwrap().clear();
145 }
146
147 pub fn len(&self) -> usize {
151 self.data.lock().unwrap().len()
152 }
153
154 pub fn is_empty(&self) -> bool {
156 self.len() == 0
157 }
158
159 pub fn get_record(&self, id: &str) -> Option<Record<CborValue>> {
164 self.data.lock().unwrap().get(id).cloned()
165 }
166
167 pub fn record_ids(&self) -> Vec<String> {
170 self.data.lock().unwrap().keys().cloned().collect()
171 }
172
173 fn guard_reads(&self) -> Result<()> {
175 if self.fail_reads.load(Ordering::SeqCst) {
176 return Err(vantage_core::error!("mock source read failed (injected)"));
177 }
178 Ok(())
179 }
180
181 fn matches_filters(&self, record: &Record<CborValue>) -> bool {
182 self.filters
183 .lock()
184 .unwrap()
185 .iter()
186 .all(|(field, expected)| record.get(field) == Some(expected))
187 }
188
189 fn matches_search(&self, record: &Record<CborValue>) -> bool {
190 let guard = self.search.lock().unwrap();
191 let Some(needle) = guard.as_deref() else {
192 return true;
193 };
194 let needle_lc = needle.to_lowercase();
195 record.values().any(|v| match v {
196 CborValue::Text(s) => s.to_lowercase().contains(&needle_lc),
197 _ => false,
198 })
199 }
200
201 fn next_auto_id(&self) -> String {
202 let mut next = self.next_auto_id.lock().unwrap();
203 let id = next.to_string();
204 *next += 1;
205 id
206 }
207}
208
209impl Default for MockShell {
210 fn default() -> Self {
211 Self::new()
212 }
213}
214
215#[async_trait]
216impl TableShell for MockShell {
217 fn columns(&self) -> &IndexMap<String, Column> {
218 &self.metadata.columns
219 }
220
221 fn references(&self) -> &IndexMap<String, Reference> {
222 &self.metadata.references
223 }
224
225 fn contained(&self) -> &IndexMap<String, ContainedSpec> {
226 &self.metadata.contained
227 }
228
229 fn get_contained_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
233 let spec = self.metadata.contained.get(relation).ok_or_else(|| {
234 vantage_core::error!("unknown contained relation", relation = relation)
235 })?;
236 let host_value = row.get(&spec.host_column).cloned();
237
238 let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
239 let parent_id = match row.get(id_field) {
240 Some(CborValue::Text(s)) => s.clone(),
241 _ => {
242 return Err(vantage_core::error!(
243 "contained traversal requires the parent row's id",
244 relation = relation
245 ));
246 }
247 };
248
249 let data = self.data.clone();
250 let host_column = spec.host_column.clone();
251 let writeback: ContainedWriteback = Arc::new(move |collection: CborValue| {
252 let data = data.clone();
253 let host_column = host_column.clone();
254 let parent_id = parent_id.clone();
255 Box::pin(async move {
256 let mut store = data.lock().unwrap();
257 if let Some(record) = store.get_mut(&parent_id) {
258 record.insert(host_column, collection);
259 }
260 Ok(())
261 })
262 });
263
264 build_contained_vista(spec, host_value.as_ref(), writeback, None)
265 }
266
267 fn get_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
272 let reference = self
273 .metadata
274 .references
275 .get(relation)
276 .ok_or_else(|| vantage_core::error!("unknown relation", relation = relation))?;
277 let target = self.ref_targets.get(relation).ok_or_else(|| {
278 vantage_core::error!("no ref target registered for relation", relation = relation)
279 })?;
280 let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
281 let parent_val = row.get(id_field).cloned().ok_or_else(|| {
282 vantage_core::error!(
283 "parent row missing id for traversal",
284 relation = relation,
285 id_field = id_field
286 )
287 })?;
288 let narrowed = target.narrowed_clone();
289 narrowed
290 .filters
291 .lock()
292 .unwrap()
293 .push((reference.foreign_key.clone(), parent_val));
294 Ok(Vista::new(reference.target.clone(), Box::new(narrowed)))
295 }
296
297 fn id_column(&self) -> Option<&str> {
298 self.metadata.id_column.as_deref()
299 }
300
301 async fn list_vista_values(
302 &self,
303 _vista: &Vista,
304 ) -> Result<IndexMap<String, Record<CborValue>>> {
305 self.guard_reads()?;
306 let data = self.data.lock().unwrap();
307 let mut rows: Vec<(String, Record<CborValue>)> = data
308 .iter()
309 .filter(|(_, record)| self.matches_filters(record) && self.matches_search(record))
310 .map(|(k, v)| (k.clone(), v.clone()))
311 .collect();
312 if let Some((field, dir)) = self.order.lock().unwrap().clone() {
313 rows.sort_by(|a, b| {
314 let lhs = a.1.get(&field);
315 let rhs = b.1.get(&field);
316 let ord = cbor_cmp(lhs, rhs);
317 match dir {
318 SortDirection::Ascending => ord,
319 SortDirection::Descending => ord.reverse(),
320 }
321 });
322 }
323 Ok(rows.into_iter().collect())
324 }
325
326 async fn fetch_window(
336 &self,
337 _vista: &Vista,
338 offset: usize,
339 limit: usize,
340 ) -> Result<Vec<(String, Record<CborValue>)>> {
341 self.guard_reads()?;
342 let data = self.data.lock().unwrap();
343 let matches =
344 |record: &Record<CborValue>| self.matches_filters(record) && self.matches_search(record);
345 let order = self.order.lock().unwrap().clone();
346 Ok(match order {
347 Some((field, dir)) => {
348 let mut refs: Vec<(&String, &Record<CborValue>)> =
349 data.iter().filter(|(_, r)| matches(r)).collect();
350 refs.sort_by(|a, b| {
351 let ord = cbor_cmp(a.1.get(&field), b.1.get(&field));
352 match dir {
353 SortDirection::Ascending => ord,
354 SortDirection::Descending => ord.reverse(),
355 }
356 });
357 refs.into_iter()
358 .skip(offset)
359 .take(limit)
360 .map(|(k, v)| (k.clone(), v.clone()))
361 .collect()
362 }
363 None => data
364 .iter()
365 .filter(|(_, r)| matches(r))
366 .skip(offset)
367 .take(limit)
368 .map(|(k, v)| (k.clone(), v.clone()))
369 .collect(),
370 })
371 }
372
373 fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
380 Some(Box::new(MockShell {
381 data: self.data.clone(),
382 next_auto_id: self.next_auto_id.clone(),
383 filters: Arc::new(Mutex::new(self.filters.lock().unwrap().clone())),
384 order: Arc::new(Mutex::new(self.order.lock().unwrap().clone())),
385 search: Arc::new(Mutex::new(self.search.lock().unwrap().clone())),
386 capabilities: self.capabilities.clone(),
387 metadata: self.metadata.clone(),
388 ref_targets: self.ref_targets.clone(),
389 fail_reads: self.fail_reads.clone(),
390 }))
391 }
392
393 async fn get_vista_value(
394 &self,
395 _vista: &Vista,
396 id: &String,
397 ) -> Result<Option<Record<CborValue>>> {
398 self.guard_reads()?;
399 Ok(self.data.lock().unwrap().get(id).cloned())
400 }
401
402 async fn get_vista_some_value(
403 &self,
404 _vista: &Vista,
405 ) -> Result<Option<(String, Record<CborValue>)>> {
406 self.guard_reads()?;
407 let data = self.data.lock().unwrap();
408 Ok(data
409 .iter()
410 .find(|(_, record)| self.matches_filters(record))
411 .map(|(k, v)| (k.clone(), v.clone())))
412 }
413
414 async fn insert_vista_value(
415 &self,
416 _vista: &Vista,
417 id: &String,
418 record: &Record<CborValue>,
419 ) -> Result<Record<CborValue>> {
420 let mut data = self.data.lock().unwrap();
421 if data.contains_key(id) {
422 return Err(vantage_core::error!("Record already exists", id = id));
423 }
424 let mut stored = record.clone();
425 stored.insert("id".to_string(), CborValue::Text(id.clone()));
426 data.insert(id.clone(), stored.clone());
427 Ok(stored)
428 }
429
430 async fn replace_vista_value(
431 &self,
432 _vista: &Vista,
433 id: &String,
434 record: &Record<CborValue>,
435 ) -> Result<Record<CborValue>> {
436 let mut data = self.data.lock().unwrap();
437 let mut stored = record.clone();
438 stored.insert("id".to_string(), CborValue::Text(id.clone()));
439 data.insert(id.clone(), stored.clone());
440 Ok(stored)
441 }
442
443 async fn patch_vista_value(
444 &self,
445 _vista: &Vista,
446 id: &String,
447 partial: &Record<CborValue>,
448 ) -> Result<Record<CborValue>> {
449 let mut data = self.data.lock().unwrap();
450 let existing = data
451 .get_mut(id)
452 .ok_or_else(|| vantage_core::error!("Record not found", id = id))?;
453 for (k, v) in partial {
454 existing.insert(k.clone(), v.clone());
455 }
456 Ok(existing.clone())
457 }
458
459 async fn delete_vista_value(&self, _vista: &Vista, id: &String) -> Result<()> {
460 let mut data = self.data.lock().unwrap();
461 if data.shift_remove(id).is_none() {
462 Err(vantage_core::error!("Record not found", id = id))
463 } else {
464 Ok(())
465 }
466 }
467
468 async fn delete_vista_all_values(&self, _vista: &Vista) -> Result<()> {
469 self.data.lock().unwrap().clear();
470 Ok(())
471 }
472
473 async fn insert_vista_return_id_value(
474 &self,
475 vista: &Vista,
476 record: &Record<CborValue>,
477 ) -> Result<String> {
478 let id = match record.get("id") {
479 Some(CborValue::Text(s)) if !s.is_empty() => s.clone(),
480 Some(CborValue::Integer(i)) => i128::from(*i).to_string(),
481 _ => self.next_auto_id(),
482 };
483 self.insert_vista_value(vista, &id, record).await?;
484 Ok(id)
485 }
486
487 async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
492 let _ = vista;
493 self.guard_reads()?;
494 let data = self.data.lock().unwrap();
495 Ok(data
496 .values()
497 .filter(|record| self.matches_filters(record) && self.matches_search(record))
498 .count() as i64)
499 }
500
501 fn capabilities(&self) -> &VistaCapabilities {
502 &self.capabilities
503 }
504
505 fn driver_name(&self) -> &'static str {
506 "mock"
507 }
508
509 fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
510 self.filters
511 .lock()
512 .unwrap()
513 .push((field.to_string(), value.clone()));
514 Ok(())
515 }
516
517 fn add_order(&mut self, field: &str, dir: SortDirection) -> Result<()> {
518 *self.order.lock().unwrap() = Some((field.to_string(), dir));
519 Ok(())
520 }
521
522 fn clear_orders(&mut self) -> Result<()> {
523 *self.order.lock().unwrap() = None;
524 Ok(())
525 }
526
527 fn add_search(&mut self, text: &str) -> Result<()> {
528 *self.search.lock().unwrap() = Some(text.to_string());
529 Ok(())
530 }
531
532 fn clear_search(&mut self) -> Result<()> {
533 *self.search.lock().unwrap() = None;
534 Ok(())
535 }
536}
537
538fn cbor_cmp(a: Option<&CborValue>, b: Option<&CborValue>) -> std::cmp::Ordering {
543 use std::cmp::Ordering;
544 match (a, b) {
545 (None, None) => Ordering::Equal,
546 (None, _) => Ordering::Less,
547 (_, None) => Ordering::Greater,
548 (Some(lhs), Some(rhs)) => match (lhs, rhs) {
549 (CborValue::Text(l), CborValue::Text(r)) => l.cmp(r),
550 (CborValue::Integer(l), CborValue::Integer(r)) => i128::from(*l).cmp(&i128::from(*r)),
551 (CborValue::Bool(l), CborValue::Bool(r)) => l.cmp(r),
552 _ => format!("{lhs:?}").cmp(&format!("{rhs:?}")),
553 },
554 }
555}
556
557#[cfg(test)]
558mod tests {
559 use super::*;
560 use crate::{Column, Reference, ReferenceKind, Vista, VistaMetadata};
561 use vantage_dataset::{InsertableValueSet, ReadableValueSet, WritableValueSet};
562
563 fn cbor_text(s: &str) -> CborValue {
564 CborValue::Text(s.into())
565 }
566
567 fn record(pairs: &[(&str, CborValue)]) -> Record<CborValue> {
568 let mut r = Record::new();
569 for (k, v) in pairs {
570 r.insert((*k).to_string(), v.clone());
571 }
572 r
573 }
574
575 fn build_user_vista(source: MockShell) -> Vista {
576 let metadata = VistaMetadata::new()
577 .with_column(Column::new("id", "String").with_flag("id"))
578 .with_column(Column::new("name", "String").with_flag("title"))
579 .with_column(Column::new("email", "String").hidden())
580 .with_column(Column::new("vip_flag", "bool"))
581 .with_id_column("id")
582 .with_reference(Reference::new(
583 "orders",
584 "orders",
585 ReferenceKind::HasMany,
586 "user_id",
587 ));
588 Vista::new("users", Box::new(source.with_metadata(metadata)))
589 }
590
591 #[test]
592 fn metadata_accessors_round_trip() {
593 let vista = build_user_vista(MockShell::new());
594
595 assert_eq!(vista.name(), "users");
596 assert_eq!(vista.get_id_column(), Some("id"));
597 assert_eq!(vista.get_title_columns(), vec!["name"]);
598 assert_eq!(
599 vista.get_column_names(),
600 vec!["id", "name", "email", "vip_flag"]
601 );
602 assert!(vista.get_column("email").unwrap().is_hidden());
603 assert!(!vista.get_column("name").unwrap().is_hidden());
604 assert_eq!(vista.get_references(), vec!["orders".to_string()]);
605 assert_eq!(
606 vista.get_reference("orders").unwrap().foreign_key,
607 "user_id"
608 );
609
610 let caps = vista.capabilities();
611 assert!(caps.can_count && caps.can_insert && caps.can_update && caps.can_delete);
612 assert!(!caps.can_subscribe);
613 }
614
615 #[tokio::test]
616 async fn list_values_returns_seeded_rows() {
617 let source = MockShell::new()
618 .with_record(
619 "1",
620 record(&[("id", cbor_text("1")), ("name", cbor_text("Alice"))]),
621 )
622 .with_record(
623 "2",
624 record(&[("id", cbor_text("2")), ("name", cbor_text("Bob"))]),
625 );
626 let vista = build_user_vista(source);
627
628 let rows = vista.list_values().await.unwrap();
629 assert_eq!(rows.len(), 2);
630 assert!(rows.contains_key("1"));
631 assert_eq!(rows["2"].get("name"), Some(&cbor_text("Bob")));
632
633 let alice = vista.get_value("1").await.unwrap().unwrap();
634 assert_eq!(alice.get("name"), Some(&cbor_text("Alice")));
635
636 assert_eq!(vista.get_count().await.unwrap(), 2);
637 }
638
639 #[tokio::test]
640 async fn add_condition_eq_filters_list_and_count() {
641 let source = MockShell::new()
642 .with_record(
643 "1",
644 record(&[
645 ("name", cbor_text("Alice")),
646 ("vip_flag", CborValue::Bool(true)),
647 ]),
648 )
649 .with_record(
650 "2",
651 record(&[
652 ("name", cbor_text("Bob")),
653 ("vip_flag", CborValue::Bool(false)),
654 ]),
655 )
656 .with_record(
657 "3",
658 record(&[
659 ("name", cbor_text("Carol")),
660 ("vip_flag", CborValue::Bool(true)),
661 ]),
662 );
663 let mut vista = build_user_vista(source);
664 vista
665 .add_condition_eq("vip_flag", CborValue::Bool(true))
666 .unwrap();
667
668 let rows = vista.list_values().await.unwrap();
669 assert_eq!(rows.len(), 2);
670 assert!(rows.contains_key("1"));
671 assert!(rows.contains_key("3"));
672 assert_eq!(vista.get_count().await.unwrap(), 2);
673 }
674
675 #[tokio::test]
676 async fn writable_value_set_round_trip() {
677 let vista = build_user_vista(MockShell::new());
678
679 let inserted = vista
681 .insert_value("alice", &record(&[("name", cbor_text("Alice"))]))
682 .await
683 .unwrap();
684 assert_eq!(inserted.get("id"), Some(&cbor_text("alice")));
685
686 let dup = vista.insert_value("alice", &record(&[])).await;
688 assert!(dup.is_err());
689
690 vista
692 .replace_value("alice", &record(&[("name", cbor_text("Alicia"))]))
693 .await
694 .unwrap();
695 let renamed = vista.get_value("alice").await.unwrap().unwrap();
696 assert_eq!(renamed.get("name"), Some(&cbor_text("Alicia")));
697
698 vista
700 .patch_value(
701 "alice",
702 &record(&[("email", cbor_text("alice@example.com"))]),
703 )
704 .await
705 .unwrap();
706 let patched = vista.get_value("alice").await.unwrap().unwrap();
707 assert_eq!(patched.get("name"), Some(&cbor_text("Alicia")));
708 assert_eq!(patched.get("email"), Some(&cbor_text("alice@example.com")));
709
710 vista.delete("alice").await.unwrap();
712 assert!(vista.get_value("alice").await.unwrap().is_none());
713
714 vista
716 .insert_value("a", &record(&[("name", cbor_text("A"))]))
717 .await
718 .unwrap();
719 vista
720 .insert_value("b", &record(&[("name", cbor_text("B"))]))
721 .await
722 .unwrap();
723 vista.delete_all().await.unwrap();
724 assert_eq!(vista.list_values().await.unwrap().len(), 0);
725 }
726
727 #[tokio::test]
728 async fn default_get_value_with_row_ignores_row_and_delegates() {
729 let source = MockShell::new().with_record(
732 "x",
733 record(&[("id", cbor_text("x")), ("name", cbor_text("Xavier"))]),
734 );
735 let vista = build_user_vista(source);
736
737 let mut row: Record<CborValue> = Record::new();
738 row.insert("extra".into(), cbor_text("ignored"));
739
740 let got = vista.get_value_with_row("x", &row).await.unwrap().unwrap();
741 assert_eq!(got.get("name"), Some(&cbor_text("Xavier")));
742 }
743
744 #[tokio::test]
745 async fn insertable_value_set_assigns_ids() {
746 let vista = build_user_vista(MockShell::new());
747
748 let auto_id = vista
750 .insert_return_id_value(&record(&[("name", cbor_text("Bob"))]))
751 .await
752 .unwrap();
753 assert_eq!(auto_id, "1");
754
755 let explicit = vista
757 .insert_return_id_value(&record(&[
758 ("id", cbor_text("alice")),
759 ("name", cbor_text("Alice")),
760 ]))
761 .await
762 .unwrap();
763 assert_eq!(explicit, "alice");
764
765 assert_eq!(vista.get_count().await.unwrap(), 2);
766 }
767}