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 id_prefix: Option<String>,
44}
45
46impl MockShell {
47 pub fn new() -> Self {
48 Self {
49 data: Arc::new(Mutex::new(IndexMap::new())),
50 next_auto_id: Arc::new(Mutex::new(1)),
51 filters: Arc::new(Mutex::new(Vec::new())),
52 order: Arc::new(Mutex::new(None)),
53 search: Arc::new(Mutex::new(None)),
54 capabilities: VistaCapabilities {
55 can_count: true,
56 can_insert: true,
57 can_update: true,
58 can_delete: true,
59 can_order: true,
60 can_search: true,
61 ..VistaCapabilities::default()
62 },
63 metadata: VistaMetadata::new(),
64 ref_targets: IndexMap::new(),
65 fail_reads: Arc::new(AtomicBool::new(false)),
66 id_prefix: None,
67 }
68 }
69
70 pub fn with_ref_target(mut self, relation: impl Into<String>, target: MockShell) -> Self {
75 self.ref_targets.insert(relation.into(), target);
76 self
77 }
78
79 pub fn set_fail_reads(&self, fail: bool) {
84 self.fail_reads.store(fail, Ordering::SeqCst);
85 }
86
87 fn narrowed_clone(&self) -> Self {
91 Self {
92 data: self.data.clone(),
93 next_auto_id: self.next_auto_id.clone(),
94 filters: Arc::new(Mutex::new(Vec::new())),
95 order: Arc::new(Mutex::new(None)),
96 search: Arc::new(Mutex::new(None)),
97 capabilities: self.capabilities.clone(),
98 metadata: self.metadata.clone(),
99 ref_targets: self.ref_targets.clone(),
100 fail_reads: self.fail_reads.clone(),
101 id_prefix: self.id_prefix.clone(),
102 }
103 }
104
105 pub fn with_capabilities(mut self, capabilities: VistaCapabilities) -> Self {
106 self.capabilities = capabilities;
107 self
108 }
109
110 pub fn with_metadata(mut self, metadata: VistaMetadata) -> Self {
111 self.metadata = metadata;
112 self
113 }
114
115 pub fn with_id_prefix(mut self, prefix: impl Into<String>) -> Self {
120 self.id_prefix = Some(prefix.into());
121 self
122 }
123
124 fn key(&self, id: &str) -> String {
126 match &self.id_prefix {
127 Some(prefix) if !id.starts_with(prefix.as_str()) => format!("{prefix}{id}"),
128 _ => id.to_string(),
129 }
130 }
131
132 pub fn with_record(self, id: impl Into<String>, record: Record<CborValue>) -> Self {
134 self.data.lock().unwrap().insert(id.into(), record);
135 self
136 }
137
138 pub fn set_record(&self, id: impl Into<String>, record: Record<CborValue>) {
149 self.data.lock().unwrap().insert(id.into(), record);
150 }
151
152 pub fn set_field(&self, id: &str, field: &str, value: CborValue) {
155 if let Some(rec) = self.data.lock().unwrap().get_mut(id) {
156 rec.insert(field.to_string(), value);
157 }
158 }
159
160 pub fn remove_record(&self, id: &str) {
162 self.data.lock().unwrap().shift_remove(id);
163 }
164
165 pub fn clear_records(&self) {
167 self.data.lock().unwrap().clear();
168 }
169
170 pub fn len(&self) -> usize {
174 self.data.lock().unwrap().len()
175 }
176
177 pub fn is_empty(&self) -> bool {
179 self.len() == 0
180 }
181
182 pub fn get_record(&self, id: &str) -> Option<Record<CborValue>> {
187 self.data.lock().unwrap().get(id).cloned()
188 }
189
190 pub fn record_ids(&self) -> Vec<String> {
193 self.data.lock().unwrap().keys().cloned().collect()
194 }
195
196 fn guard_reads(&self) -> Result<()> {
198 if self.fail_reads.load(Ordering::SeqCst) {
199 return Err(vantage_core::error!("mock source read failed (injected)"));
200 }
201 Ok(())
202 }
203
204 fn matches_filters(&self, record: &Record<CborValue>) -> bool {
205 self.filters
206 .lock()
207 .unwrap()
208 .iter()
209 .all(|(field, expected)| record.get(field) == Some(expected))
210 }
211
212 fn matches_search(&self, record: &Record<CborValue>) -> bool {
213 let guard = self.search.lock().unwrap();
214 let Some(needle) = guard.as_deref() else {
215 return true;
216 };
217 let needle_lc = needle.to_lowercase();
218 record.values().any(|v| match v {
219 CborValue::Text(s) => s.to_lowercase().contains(&needle_lc),
220 _ => false,
221 })
222 }
223
224 fn next_auto_id(&self) -> String {
225 let mut next = self.next_auto_id.lock().unwrap();
226 let id = next.to_string();
227 *next += 1;
228 id
229 }
230}
231
232impl Default for MockShell {
233 fn default() -> Self {
234 Self::new()
235 }
236}
237
238#[async_trait]
239impl TableShell for MockShell {
240 fn columns(&self) -> &IndexMap<String, Column> {
241 &self.metadata.columns
242 }
243
244 fn references(&self) -> &IndexMap<String, Reference> {
245 &self.metadata.references
246 }
247
248 fn contained(&self) -> &IndexMap<String, ContainedSpec> {
249 &self.metadata.contained
250 }
251
252 fn get_contained_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
256 let spec = self.metadata.contained.get(relation).ok_or_else(|| {
257 vantage_core::error!("unknown contained relation", relation = relation)
258 })?;
259 let host_value = row.get(&spec.host_column).cloned();
260
261 let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
262 let parent_id = match row.get(id_field) {
263 Some(CborValue::Text(s)) => s.clone(),
264 _ => {
265 return Err(vantage_core::error!(
266 "contained traversal requires the parent row's id",
267 relation = relation
268 ));
269 }
270 };
271
272 let data = self.data.clone();
273 let host_column = spec.host_column.clone();
274 let writeback: ContainedWriteback = Arc::new(move |collection: CborValue| {
275 let data = data.clone();
276 let host_column = host_column.clone();
277 let parent_id = parent_id.clone();
278 Box::pin(async move {
279 let mut store = data.lock().unwrap();
280 if let Some(record) = store.get_mut(&parent_id) {
281 record.insert(host_column, collection);
282 }
283 Ok(())
284 })
285 });
286
287 build_contained_vista(spec, host_value.as_ref(), writeback, None)
288 }
289
290 fn get_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
295 let reference = self
296 .metadata
297 .references
298 .get(relation)
299 .ok_or_else(|| vantage_core::error!("unknown relation", relation = relation))?;
300 let target = self.ref_targets.get(relation).ok_or_else(|| {
301 vantage_core::error!("no ref target registered for relation", relation = relation)
302 })?;
303 let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
304 let parent_val = row.get(id_field).cloned().ok_or_else(|| {
305 vantage_core::error!(
306 "parent row missing id for traversal",
307 relation = relation,
308 id_field = id_field
309 )
310 })?;
311 let narrowed = target.narrowed_clone();
312 narrowed
313 .filters
314 .lock()
315 .unwrap()
316 .push((reference.foreign_key.clone(), parent_val));
317 Ok(Vista::new(reference.target.clone(), Box::new(narrowed)))
318 }
319
320 fn id_column(&self) -> Option<&str> {
321 self.metadata.id_column.as_deref()
322 }
323
324 async fn list_vista_values(
325 &self,
326 _vista: &Vista,
327 ) -> Result<IndexMap<String, Record<CborValue>>> {
328 self.guard_reads()?;
329 let data = self.data.lock().unwrap();
330 let mut rows: Vec<(String, Record<CborValue>)> = data
331 .iter()
332 .filter(|(_, record)| self.matches_filters(record) && self.matches_search(record))
333 .map(|(k, v)| (k.clone(), v.clone()))
334 .collect();
335 if let Some((field, dir)) = self.order.lock().unwrap().clone() {
336 rows.sort_by(|a, b| {
337 let lhs = a.1.get(&field);
338 let rhs = b.1.get(&field);
339 let ord = cbor_cmp(lhs, rhs);
340 match dir {
341 SortDirection::Ascending => ord,
342 SortDirection::Descending => ord.reverse(),
343 }
344 });
345 }
346 Ok(rows.into_iter().collect())
347 }
348
349 async fn fetch_window(
359 &self,
360 _vista: &Vista,
361 offset: usize,
362 limit: usize,
363 ) -> Result<Vec<(String, Record<CborValue>)>> {
364 self.guard_reads()?;
365 let data = self.data.lock().unwrap();
366 let matches = |record: &Record<CborValue>| {
367 self.matches_filters(record) && self.matches_search(record)
368 };
369 let order = self.order.lock().unwrap().clone();
370 Ok(match order {
371 Some((field, dir)) => {
372 let mut refs: Vec<(&String, &Record<CborValue>)> =
373 data.iter().filter(|(_, r)| matches(r)).collect();
374 refs.sort_by(|a, b| {
375 let ord = cbor_cmp(a.1.get(&field), b.1.get(&field));
376 match dir {
377 SortDirection::Ascending => ord,
378 SortDirection::Descending => ord.reverse(),
379 }
380 });
381 refs.into_iter()
382 .skip(offset)
383 .take(limit)
384 .map(|(k, v)| (k.clone(), v.clone()))
385 .collect()
386 }
387 None => data
388 .iter()
389 .filter(|(_, r)| matches(r))
390 .skip(offset)
391 .take(limit)
392 .map(|(k, v)| (k.clone(), v.clone()))
393 .collect(),
394 })
395 }
396
397 fn clone_shell(&self) -> Option<Box<dyn TableShell>> {
404 Some(Box::new(MockShell {
405 data: self.data.clone(),
406 next_auto_id: self.next_auto_id.clone(),
407 filters: Arc::new(Mutex::new(self.filters.lock().unwrap().clone())),
408 order: Arc::new(Mutex::new(self.order.lock().unwrap().clone())),
409 search: Arc::new(Mutex::new(self.search.lock().unwrap().clone())),
410 capabilities: self.capabilities.clone(),
411 metadata: self.metadata.clone(),
412 ref_targets: self.ref_targets.clone(),
413 fail_reads: self.fail_reads.clone(),
414 id_prefix: self.id_prefix.clone(),
415 }))
416 }
417
418 async fn get_vista_value(
419 &self,
420 _vista: &Vista,
421 id: &String,
422 ) -> Result<Option<Record<CborValue>>> {
423 self.guard_reads()?;
424 Ok(self.data.lock().unwrap().get(&self.key(id)).cloned())
425 }
426
427 async fn get_vista_some_value(
428 &self,
429 _vista: &Vista,
430 ) -> Result<Option<(String, Record<CborValue>)>> {
431 self.guard_reads()?;
432 let data = self.data.lock().unwrap();
433 Ok(data
434 .iter()
435 .find(|(_, record)| self.matches_filters(record))
436 .map(|(k, v)| (k.clone(), v.clone())))
437 }
438
439 async fn insert_vista_value(
446 &self,
447 _vista: &Vista,
448 id: &String,
449 record: &Record<CborValue>,
450 ) -> Result<Record<CborValue>> {
451 let key = self.key(id);
452 let mut data = self.data.lock().unwrap();
453 if data.contains_key(&key) {
454 return Err(vantage_core::error!("Record already exists", id = id));
455 }
456 let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
457 let mut stored = record.clone();
458 stored.insert(id_field.to_string(), CborValue::Text(key.clone()));
459 data.insert(key, stored.clone());
460 Ok(stored)
461 }
462
463 async fn replace_vista_value(
464 &self,
465 _vista: &Vista,
466 id: &String,
467 record: &Record<CborValue>,
468 ) -> Result<Record<CborValue>> {
469 let key = self.key(id);
470 let id_field = self.metadata.id_column.as_deref().unwrap_or("id");
471 let mut data = self.data.lock().unwrap();
472 let mut stored = record.clone();
473 stored.insert(id_field.to_string(), CborValue::Text(key.clone()));
474 data.insert(key, stored.clone());
475 Ok(stored)
476 }
477
478 async fn patch_vista_value(
479 &self,
480 _vista: &Vista,
481 id: &String,
482 partial: &Record<CborValue>,
483 ) -> Result<Record<CborValue>> {
484 let key = self.key(id);
485 let mut data = self.data.lock().unwrap();
486 let existing = data
487 .get_mut(&key)
488 .ok_or_else(|| vantage_core::error!("Record not found", id = id))?;
489 for (k, v) in partial {
490 existing.insert(k.clone(), v.clone());
491 }
492 Ok(existing.clone())
493 }
494
495 async fn delete_vista_value(&self, _vista: &Vista, id: &String) -> Result<()> {
496 let key = self.key(id);
497 let mut data = self.data.lock().unwrap();
498 if data.shift_remove(&key).is_none() {
499 Err(vantage_core::error!("Record not found", id = id))
500 } else {
501 Ok(())
502 }
503 }
504
505 async fn delete_vista_all_values(&self, _vista: &Vista) -> Result<()> {
506 self.data.lock().unwrap().clear();
507 Ok(())
508 }
509
510 async fn insert_vista_return_id_value(
514 &self,
515 vista: &Vista,
516 record: &Record<CborValue>,
517 ) -> Result<String> {
518 let id = match record.get("id") {
519 Some(CborValue::Text(s)) if !s.is_empty() => s.clone(),
520 Some(CborValue::Integer(i)) => i128::from(*i).to_string(),
521 _ => self.next_auto_id(),
522 };
523 self.insert_vista_value(vista, &id, record).await?;
524 Ok(self.key(&id))
525 }
526
527 async fn get_vista_count(&self, vista: &Vista) -> Result<i64> {
532 let _ = vista;
533 self.guard_reads()?;
534 let data = self.data.lock().unwrap();
535 Ok(data
536 .values()
537 .filter(|record| self.matches_filters(record) && self.matches_search(record))
538 .count() as i64)
539 }
540
541 fn capabilities(&self) -> &VistaCapabilities {
542 &self.capabilities
543 }
544
545 fn driver_name(&self) -> &'static str {
546 "mock"
547 }
548
549 fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
550 self.filters
551 .lock()
552 .unwrap()
553 .push((field.to_string(), value.clone()));
554 Ok(())
555 }
556
557 fn add_order(&mut self, field: &str, dir: SortDirection) -> Result<()> {
558 *self.order.lock().unwrap() = Some((field.to_string(), dir));
559 Ok(())
560 }
561
562 fn clear_orders(&mut self) -> Result<()> {
563 *self.order.lock().unwrap() = None;
564 Ok(())
565 }
566
567 fn add_search(&mut self, text: &str) -> Result<()> {
568 *self.search.lock().unwrap() = Some(text.to_string());
569 Ok(())
570 }
571
572 fn clear_search(&mut self) -> Result<()> {
573 *self.search.lock().unwrap() = None;
574 Ok(())
575 }
576}
577
578fn cbor_cmp(a: Option<&CborValue>, b: Option<&CborValue>) -> std::cmp::Ordering {
583 use std::cmp::Ordering;
584 match (a, b) {
585 (None, None) => Ordering::Equal,
586 (None, _) => Ordering::Less,
587 (_, None) => Ordering::Greater,
588 (Some(lhs), Some(rhs)) => match (lhs, rhs) {
589 (CborValue::Text(l), CborValue::Text(r)) => l.cmp(r),
590 (CborValue::Integer(l), CborValue::Integer(r)) => i128::from(*l).cmp(&i128::from(*r)),
591 (CborValue::Bool(l), CborValue::Bool(r)) => l.cmp(r),
592 _ => format!("{lhs:?}").cmp(&format!("{rhs:?}")),
593 },
594 }
595}
596
597#[cfg(test)]
598mod tests {
599 use super::*;
600 use crate::{Column, Reference, ReferenceKind, Vista, VistaMetadata};
601 use vantage_dataset::{InsertableValueSet, ReadableValueSet, WritableValueSet};
602
603 fn cbor_text(s: &str) -> CborValue {
604 CborValue::Text(s.into())
605 }
606
607 fn record(pairs: &[(&str, CborValue)]) -> Record<CborValue> {
608 let mut r = Record::new();
609 for (k, v) in pairs {
610 r.insert((*k).to_string(), v.clone());
611 }
612 r
613 }
614
615 fn build_user_vista(source: MockShell) -> Vista {
616 let metadata = VistaMetadata::new()
617 .with_column(Column::new("id", "String").with_flag("id"))
618 .with_column(Column::new("name", "String").with_flag("title"))
619 .with_column(Column::new("email", "String").hidden())
620 .with_column(Column::new("vip_flag", "bool"))
621 .with_id_column("id")
622 .with_reference(Reference::new(
623 "orders",
624 "orders",
625 ReferenceKind::HasMany,
626 "user_id",
627 ));
628 Vista::new("users", Box::new(source.with_metadata(metadata)))
629 }
630
631 #[test]
632 fn metadata_accessors_round_trip() {
633 let vista = build_user_vista(MockShell::new());
634
635 assert_eq!(vista.name(), "users");
636 assert_eq!(vista.get_id_column(), Some("id"));
637 assert_eq!(vista.get_title_columns(), vec!["name"]);
638 assert_eq!(
639 vista.get_column_names(),
640 vec!["id", "name", "email", "vip_flag"]
641 );
642 assert!(vista.get_column("email").unwrap().is_hidden());
643 assert!(!vista.get_column("name").unwrap().is_hidden());
644 assert_eq!(vista.get_references(), vec!["orders".to_string()]);
645 assert_eq!(
646 vista.get_reference("orders").unwrap().foreign_key,
647 "user_id"
648 );
649
650 let caps = vista.capabilities();
651 assert!(caps.can_count && caps.can_insert && caps.can_update && caps.can_delete);
652 assert!(!caps.can_subscribe);
653 }
654
655 #[tokio::test]
656 async fn list_values_returns_seeded_rows() {
657 let source = MockShell::new()
658 .with_record(
659 "1",
660 record(&[("id", cbor_text("1")), ("name", cbor_text("Alice"))]),
661 )
662 .with_record(
663 "2",
664 record(&[("id", cbor_text("2")), ("name", cbor_text("Bob"))]),
665 );
666 let vista = build_user_vista(source);
667
668 let rows = vista.list_values().await.unwrap();
669 assert_eq!(rows.len(), 2);
670 assert!(rows.contains_key("1"));
671 assert_eq!(rows["2"].get("name"), Some(&cbor_text("Bob")));
672
673 let alice = vista.get_value("1").await.unwrap().unwrap();
674 assert_eq!(alice.get("name"), Some(&cbor_text("Alice")));
675
676 assert_eq!(vista.get_count().await.unwrap(), 2);
677 }
678
679 #[tokio::test]
680 async fn add_condition_eq_filters_list_and_count() {
681 let source = MockShell::new()
682 .with_record(
683 "1",
684 record(&[
685 ("name", cbor_text("Alice")),
686 ("vip_flag", CborValue::Bool(true)),
687 ]),
688 )
689 .with_record(
690 "2",
691 record(&[
692 ("name", cbor_text("Bob")),
693 ("vip_flag", CborValue::Bool(false)),
694 ]),
695 )
696 .with_record(
697 "3",
698 record(&[
699 ("name", cbor_text("Carol")),
700 ("vip_flag", CborValue::Bool(true)),
701 ]),
702 );
703 let mut vista = build_user_vista(source);
704 vista
705 .add_condition_eq("vip_flag", CborValue::Bool(true))
706 .unwrap();
707
708 let rows = vista.list_values().await.unwrap();
709 assert_eq!(rows.len(), 2);
710 assert!(rows.contains_key("1"));
711 assert!(rows.contains_key("3"));
712 assert_eq!(vista.get_count().await.unwrap(), 2);
713 }
714
715 #[tokio::test]
716 async fn writable_value_set_round_trip() {
717 let vista = build_user_vista(MockShell::new());
718
719 let inserted = vista
721 .insert_value("alice", &record(&[("name", cbor_text("Alice"))]))
722 .await
723 .unwrap();
724 assert_eq!(inserted.get("id"), Some(&cbor_text("alice")));
725
726 let dup = vista.insert_value("alice", &record(&[])).await;
728 assert!(dup.is_err());
729
730 vista
732 .replace_value("alice", &record(&[("name", cbor_text("Alicia"))]))
733 .await
734 .unwrap();
735 let renamed = vista.get_value("alice").await.unwrap().unwrap();
736 assert_eq!(renamed.get("name"), Some(&cbor_text("Alicia")));
737
738 vista
740 .patch_value(
741 "alice",
742 &record(&[("email", cbor_text("alice@example.com"))]),
743 )
744 .await
745 .unwrap();
746 let patched = vista.get_value("alice").await.unwrap().unwrap();
747 assert_eq!(patched.get("name"), Some(&cbor_text("Alicia")));
748 assert_eq!(patched.get("email"), Some(&cbor_text("alice@example.com")));
749
750 vista.delete("alice").await.unwrap();
752 assert!(vista.get_value("alice").await.unwrap().is_none());
753
754 vista
756 .insert_value("a", &record(&[("name", cbor_text("A"))]))
757 .await
758 .unwrap();
759 vista
760 .insert_value("b", &record(&[("name", cbor_text("B"))]))
761 .await
762 .unwrap();
763 vista.delete_all().await.unwrap();
764 assert_eq!(vista.list_values().await.unwrap().len(), 0);
765 }
766
767 #[tokio::test]
768 async fn default_get_value_with_row_ignores_row_and_delegates() {
769 let source = MockShell::new().with_record(
772 "x",
773 record(&[("id", cbor_text("x")), ("name", cbor_text("Xavier"))]),
774 );
775 let vista = build_user_vista(source);
776
777 let mut row: Record<CborValue> = Record::new();
778 row.insert("extra".into(), cbor_text("ignored"));
779
780 let got = vista.get_value_with_row("x", &row).await.unwrap().unwrap();
781 assert_eq!(got.get("name"), Some(&cbor_text("Xavier")));
782 }
783
784 #[tokio::test]
785 async fn insertable_value_set_assigns_ids() {
786 let vista = build_user_vista(MockShell::new());
787
788 let auto_id = vista
790 .insert_return_id_value(&record(&[("name", cbor_text("Bob"))]))
791 .await
792 .unwrap();
793 assert_eq!(auto_id, "1");
794
795 let explicit = vista
797 .insert_return_id_value(&record(&[
798 ("id", cbor_text("alice")),
799 ("name", cbor_text("Alice")),
800 ]))
801 .await
802 .unwrap();
803 assert_eq!(explicit, "alice");
804
805 assert_eq!(vista.get_count().await.unwrap(), 2);
806 }
807}