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