Skip to main content

vantage_vista/mocks/
mock_shell.rs

1//! In-memory `TableShell` for tests and examples.
2
3use 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    /// Seed a record with an explicit id.
65    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    /// Resolve a contained relation against `row`, with a writeback that patches
119    /// the parent record's host column directly in this mock's store — the
120    /// in-memory analogue of a driver patching its row.
121    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
317/// Total-order comparator for the CBOR scalars MockShell records carry.
318/// Falls back to lexical ordering of CBOR-as-text for mixed-or-unknown
319/// types, which keeps sort deterministic without claiming semantic
320/// equivalence between heterogeneous values.
321fn 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        // insert_value with explicit id
459        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        // duplicate insert_value fails
469        let dup = vista.insert_value(&"alice".to_string(), &record(&[])).await;
470        assert!(dup.is_err());
471
472        // replace_value upserts
473        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        // patch_value merges
488        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        // delete
504        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        // delete_all
514        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        // A driver that does not override `get_vista_value_with_row` must behave
529        // exactly like `get_value` — the extra `row` is ignored.
530        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        // record without id → mock generates one
552        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        // record with explicit string id → preserved
559        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}