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 preview_query(&self, vista: &Vista) -> serde_json::Value {
553 let filters: Vec<String> = self
554 .filters
555 .lock()
556 .unwrap()
557 .iter()
558 .map(|(field, value)| format!("{field} = {value:?}"))
559 .collect();
560 let order = self.order.lock().unwrap().as_ref().map(|(col, dir)| {
561 let dir = match dir {
562 SortDirection::Ascending => "asc",
563 SortDirection::Descending => "desc",
564 };
565 format!("{col} {dir}")
566 });
567 serde_json::json!({
568 "driver": "mock",
569 "table": vista.name(),
570 "filters": filters,
571 "order": order,
572 "search": self.search.lock().unwrap().clone(),
573 })
574 }
575
576 fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
577 self.filters
578 .lock()
579 .unwrap()
580 .push((field.to_string(), value.clone()));
581 Ok(())
582 }
583
584 fn add_order(&mut self, field: &str, dir: SortDirection) -> Result<()> {
585 *self.order.lock().unwrap() = Some((field.to_string(), dir));
586 Ok(())
587 }
588
589 fn clear_orders(&mut self) -> Result<()> {
590 *self.order.lock().unwrap() = None;
591 Ok(())
592 }
593
594 fn add_search(&mut self, text: &str) -> Result<()> {
595 *self.search.lock().unwrap() = Some(text.to_string());
596 Ok(())
597 }
598
599 fn clear_search(&mut self) -> Result<()> {
600 *self.search.lock().unwrap() = None;
601 Ok(())
602 }
603}
604
605fn cbor_cmp(a: Option<&CborValue>, b: Option<&CborValue>) -> std::cmp::Ordering {
610 use std::cmp::Ordering;
611 match (a, b) {
612 (None, None) => Ordering::Equal,
613 (None, _) => Ordering::Less,
614 (_, None) => Ordering::Greater,
615 (Some(lhs), Some(rhs)) => match (lhs, rhs) {
616 (CborValue::Text(l), CborValue::Text(r)) => l.cmp(r),
617 (CborValue::Integer(l), CborValue::Integer(r)) => i128::from(*l).cmp(&i128::from(*r)),
618 (CborValue::Bool(l), CborValue::Bool(r)) => l.cmp(r),
619 _ => format!("{lhs:?}").cmp(&format!("{rhs:?}")),
620 },
621 }
622}
623
624#[cfg(test)]
625mod tests {
626 use super::*;
627 use crate::{Column, Reference, ReferenceKind, Vista, VistaMetadata};
628 use vantage_dataset::{InsertableValueSet, ReadableValueSet, WritableValueSet};
629
630 fn cbor_text(s: &str) -> CborValue {
631 CborValue::Text(s.into())
632 }
633
634 fn record(pairs: &[(&str, CborValue)]) -> Record<CborValue> {
635 let mut r = Record::new();
636 for (k, v) in pairs {
637 r.insert((*k).to_string(), v.clone());
638 }
639 r
640 }
641
642 fn build_user_vista(source: MockShell) -> Vista {
643 let metadata = VistaMetadata::new()
644 .with_column(Column::new("id", "String").with_flag("id"))
645 .with_column(Column::new("name", "String").with_flag("title"))
646 .with_column(Column::new("email", "String").hidden())
647 .with_column(Column::new("vip_flag", "bool"))
648 .with_id_column("id")
649 .with_reference(Reference::new(
650 "orders",
651 "orders",
652 ReferenceKind::HasMany,
653 "user_id",
654 ));
655 Vista::new("users", Box::new(source.with_metadata(metadata)))
656 }
657
658 #[test]
659 fn metadata_accessors_round_trip() {
660 let vista = build_user_vista(MockShell::new());
661
662 assert_eq!(vista.name(), "users");
663 assert_eq!(vista.get_id_column(), Some("id"));
664 assert_eq!(vista.get_title_columns(), vec!["name"]);
665 assert_eq!(
666 vista.get_column_names(),
667 vec!["id", "name", "email", "vip_flag"]
668 );
669 assert!(vista.get_column("email").unwrap().is_hidden());
670 assert!(!vista.get_column("name").unwrap().is_hidden());
671 assert_eq!(vista.get_references(), vec!["orders".to_string()]);
672 assert_eq!(
673 vista.get_reference("orders").unwrap().foreign_key,
674 "user_id"
675 );
676
677 let caps = vista.capabilities();
678 assert!(caps.can_count && caps.can_insert && caps.can_update && caps.can_delete);
679 assert!(!caps.can_subscribe);
680 }
681
682 #[tokio::test]
683 async fn list_values_returns_seeded_rows() {
684 let source = MockShell::new()
685 .with_record(
686 "1",
687 record(&[("id", cbor_text("1")), ("name", cbor_text("Alice"))]),
688 )
689 .with_record(
690 "2",
691 record(&[("id", cbor_text("2")), ("name", cbor_text("Bob"))]),
692 );
693 let vista = build_user_vista(source);
694
695 let rows = vista.list_values().await.unwrap();
696 assert_eq!(rows.len(), 2);
697 assert!(rows.contains_key("1"));
698 assert_eq!(rows["2"].get("name"), Some(&cbor_text("Bob")));
699
700 let alice = vista.get_value("1").await.unwrap().unwrap();
701 assert_eq!(alice.get("name"), Some(&cbor_text("Alice")));
702
703 assert_eq!(vista.get_count().await.unwrap(), 2);
704 }
705
706 #[tokio::test]
707 async fn add_condition_eq_filters_list_and_count() {
708 let source = MockShell::new()
709 .with_record(
710 "1",
711 record(&[
712 ("name", cbor_text("Alice")),
713 ("vip_flag", CborValue::Bool(true)),
714 ]),
715 )
716 .with_record(
717 "2",
718 record(&[
719 ("name", cbor_text("Bob")),
720 ("vip_flag", CborValue::Bool(false)),
721 ]),
722 )
723 .with_record(
724 "3",
725 record(&[
726 ("name", cbor_text("Carol")),
727 ("vip_flag", CborValue::Bool(true)),
728 ]),
729 );
730 let mut vista = build_user_vista(source);
731 vista
732 .add_condition_eq("vip_flag", CborValue::Bool(true))
733 .unwrap();
734
735 let rows = vista.list_values().await.unwrap();
736 assert_eq!(rows.len(), 2);
737 assert!(rows.contains_key("1"));
738 assert!(rows.contains_key("3"));
739 assert_eq!(vista.get_count().await.unwrap(), 2);
740 }
741
742 #[tokio::test]
743 async fn writable_value_set_round_trip() {
744 let vista = build_user_vista(MockShell::new());
745
746 let inserted = vista
748 .insert_value("alice", &record(&[("name", cbor_text("Alice"))]))
749 .await
750 .unwrap();
751 assert_eq!(inserted.get("id"), Some(&cbor_text("alice")));
752
753 let dup = vista.insert_value("alice", &record(&[])).await;
755 assert!(dup.is_err());
756
757 vista
759 .replace_value("alice", &record(&[("name", cbor_text("Alicia"))]))
760 .await
761 .unwrap();
762 let renamed = vista.get_value("alice").await.unwrap().unwrap();
763 assert_eq!(renamed.get("name"), Some(&cbor_text("Alicia")));
764
765 vista
767 .patch_value(
768 "alice",
769 &record(&[("email", cbor_text("alice@example.com"))]),
770 )
771 .await
772 .unwrap();
773 let patched = vista.get_value("alice").await.unwrap().unwrap();
774 assert_eq!(patched.get("name"), Some(&cbor_text("Alicia")));
775 assert_eq!(patched.get("email"), Some(&cbor_text("alice@example.com")));
776
777 vista.delete("alice").await.unwrap();
779 assert!(vista.get_value("alice").await.unwrap().is_none());
780
781 vista
783 .insert_value("a", &record(&[("name", cbor_text("A"))]))
784 .await
785 .unwrap();
786 vista
787 .insert_value("b", &record(&[("name", cbor_text("B"))]))
788 .await
789 .unwrap();
790 vista.delete_all().await.unwrap();
791 assert_eq!(vista.list_values().await.unwrap().len(), 0);
792 }
793
794 #[tokio::test]
795 async fn default_get_value_with_row_ignores_row_and_delegates() {
796 let source = MockShell::new().with_record(
799 "x",
800 record(&[("id", cbor_text("x")), ("name", cbor_text("Xavier"))]),
801 );
802 let vista = build_user_vista(source);
803
804 let mut row: Record<CborValue> = Record::new();
805 row.insert("extra".into(), cbor_text("ignored"));
806
807 let got = vista.get_value_with_row("x", &row).await.unwrap().unwrap();
808 assert_eq!(got.get("name"), Some(&cbor_text("Xavier")));
809 }
810
811 #[tokio::test]
812 async fn insertable_value_set_assigns_ids() {
813 let vista = build_user_vista(MockShell::new());
814
815 let auto_id = vista
817 .insert_return_id_value(&record(&[("name", cbor_text("Bob"))]))
818 .await
819 .unwrap();
820 assert_eq!(auto_id, "1");
821
822 let explicit = vista
824 .insert_return_id_value(&record(&[
825 ("id", cbor_text("alice")),
826 ("name", cbor_text("Alice")),
827 ]))
828 .await
829 .unwrap();
830 assert_eq!(explicit, "alice");
831
832 assert_eq!(vista.get_count().await.unwrap(), 2);
833 }
834}