Skip to main content

vantage_table/
any.rs

1//! Type-erased table wrapper with downcasting support
2//!
3//! `AnyTable` provides a way to store tables of different types uniformly
4//! while preserving the ability to recover the concrete type through downcasting.
5//!
6//! Tables whose value/id types differ from `ciborium::Value`/`String` can still
7//! be wrapped via [`AnyTable::from_table`] as long as the value type implements
8//! `Into<CborValue> + From<CborValue>` and the id type implements
9//! `Display + From<String>`.
10//!
11//! Each backend brings its own native value type (`AnySurrealType`,
12//! `AnyMongoType`, `AnyPostgresType`, etc.); CBOR is just the carrier across
13//! the type-erased boundary, chosen because it preserves type fidelity that
14//! JSON loses (integer vs float, binary blobs, NaN/Infinity, precise
15//! decimals).
16
17use std::any::TypeId;
18use std::fmt::Display;
19
20use async_trait::async_trait;
21use ciborium::Value as CborValue;
22use indexmap::IndexMap;
23use vantage_core::{Result, error};
24use vantage_dataset::traits::{ReadableValueSet, ValueSet, WritableValueSet};
25use vantage_types::{Entity, Record};
26
27// Type alias for cleaner code
28pub type AnyRecord = Record<CborValue>;
29
30use crate::{
31    conditions::ConditionHandle,
32    pagination::Pagination,
33    table::Table,
34    traits::{table_like::TableLike, table_source::TableSource},
35};
36
37/// Type-erased table that can be downcast to concrete `Table<T, E>`
38/// Works with AnyRecord (which uses ciborium::Value)
39pub struct AnyTable {
40    inner: Box<dyn TableLike<Value = CborValue, Id = String>>,
41    datasource_type_id: TypeId,
42    entity_type_id: TypeId,
43    datasource_name: &'static str,
44    entity_name: &'static str,
45}
46
47impl AnyTable {
48    /// Create a new AnyTable from a concrete table.
49    /// Only works with tables that use `ciborium::Value` as their Value type.
50    pub fn new<
51        T: TableSource<Value = CborValue, Id = String> + 'static,
52        E: Entity<CborValue> + 'static,
53    >(
54        table: Table<T, E>,
55    ) -> Self {
56        Self {
57            inner: Box::new(table),
58            datasource_type_id: TypeId::of::<T>(),
59            entity_type_id: TypeId::of::<E>(),
60            datasource_name: std::any::type_name::<T>(),
61            entity_name: std::any::type_name::<E>(),
62        }
63    }
64
65    /// Create an AnyTable from a table with any value/id types that convert
66    /// to/from CBOR.
67    ///
68    /// This wraps the table in an internal adapter that converts values on the
69    /// fly, so any `TableSource` can be used with the unified `AnyTable`
70    /// interface.
71    pub fn from_table<T, E>(table: Table<T, E>) -> Self
72    where
73        T: TableSource + 'static,
74        T::Value: Into<CborValue> + From<CborValue>,
75        T::Id: Display + From<String>,
76        E: Entity<T::Value> + 'static,
77    {
78        Self {
79            inner: Box::new(CborAdapter { inner: table }),
80            datasource_type_id: TypeId::of::<T>(),
81            entity_type_id: TypeId::of::<E>(),
82            datasource_name: std::any::type_name::<T>(),
83            entity_name: std::any::type_name::<E>(),
84        }
85    }
86
87    /// Wrap any `TableLike<Value = CborValue, Id = String>` directly. Use
88    /// this for table-like wrappers (e.g. `vantage_live::LiveTable`) that
89    /// aren't a `Table<T, E>` instance themselves but already satisfy the
90    /// `AnyTable`-facing trait surface. The entity type is recorded as
91    /// `()` since wrappers don't carry one.
92    pub fn from_table_like<T>(table: T) -> Self
93    where
94        T: TableLike<Value = CborValue, Id = String> + 'static,
95    {
96        Self {
97            inner: Box::new(table),
98            datasource_type_id: TypeId::of::<T>(),
99            entity_type_id: TypeId::of::<()>(),
100            datasource_name: std::any::type_name::<T>(),
101            entity_name: "<wrapped>",
102        }
103    }
104
105    /// Attempt to downcast to a concrete `Table<T, E>`
106    ///
107    /// Returns `Err(self)` if the type doesn't match, allowing recovery
108    pub fn downcast<
109        T: TableSource<Value = CborValue, Id = String> + 'static,
110        E: Entity<CborValue> + 'static,
111    >(
112        self,
113    ) -> Result<Table<T, E>> {
114        // Check TypeIds for better error messages
115        if self.datasource_type_id != TypeId::of::<T>() {
116            let expected = std::any::type_name::<T>();
117            return Err(error!(
118                "DataSource type mismatch",
119                expected = expected,
120                actual = self.datasource_name
121            ));
122        }
123        if self.entity_type_id != TypeId::of::<E>() {
124            let expected = std::any::type_name::<E>();
125            return Err(error!(
126                "Entity type mismatch",
127                expected = expected,
128                actual = self.entity_name
129            ));
130        }
131
132        // Perform the actual downcast
133        self.inner
134            .into_any()
135            .downcast::<Table<T, E>>()
136            .map(|boxed| *boxed)
137            .map_err(|_| error!("Failed to downcast table"))
138    }
139
140    /// Get the datasource type name for debugging
141    pub fn datasource_name(&self) -> &str {
142        self.datasource_name
143    }
144
145    /// Get the entity type name for debugging
146    pub fn entity_name(&self) -> &str {
147        self.entity_name
148    }
149
150    /// Get the datasource TypeId
151    pub fn datasource_type_id(&self) -> TypeId {
152        self.datasource_type_id
153    }
154
155    /// Get the entity TypeId
156    pub fn entity_type_id(&self) -> TypeId {
157        self.entity_type_id
158    }
159
160    /// Check if this table matches the given types
161    pub fn is_type<T: TableSource + 'static, E: Entity<CborValue> + 'static>(&self) -> bool {
162        self.datasource_type_id == TypeId::of::<T>() && self.entity_type_id == TypeId::of::<E>()
163    }
164}
165
166impl Clone for AnyTable {
167    fn clone(&self) -> Self {
168        Self {
169            inner: self.inner.clone_box(),
170            datasource_type_id: self.datasource_type_id,
171            entity_type_id: self.entity_type_id,
172            datasource_name: self.datasource_name,
173            entity_name: self.entity_name,
174        }
175    }
176}
177
178impl std::fmt::Debug for AnyTable {
179    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
180        f.debug_struct("AnyTable")
181            .field("datasource", &self.datasource_name)
182            .field("entity", &self.entity_name)
183            .finish()
184    }
185}
186
187// Implement ValueSet first
188impl ValueSet for AnyTable {
189    type Id = String;
190    type Value = CborValue;
191}
192
193// Implement ReadableValueSet by delegating to inner TableLike
194#[async_trait]
195impl ReadableValueSet for AnyTable {
196    async fn list_values(&self) -> Result<IndexMap<Self::Id, Record<Self::Value>>> {
197        self.inner.list_values().await
198    }
199
200    async fn get_value(&self, id: &Self::Id) -> Result<Option<Record<Self::Value>>> {
201        self.inner.get_value(id).await
202    }
203
204    async fn get_some_value(&self) -> Result<Option<(Self::Id, Record<Self::Value>)>> {
205        self.inner.get_some_value().await
206    }
207}
208
209// Implement WritableValueSet by delegating to inner TableLike
210#[async_trait]
211impl WritableValueSet for AnyTable {
212    async fn insert_value(
213        &self,
214        id: &Self::Id,
215        record: &Record<Self::Value>,
216    ) -> Result<Record<Self::Value>> {
217        self.inner.insert_value(id, record).await
218    }
219
220    async fn replace_value(
221        &self,
222        id: &Self::Id,
223        record: &Record<Self::Value>,
224    ) -> Result<Record<Self::Value>> {
225        self.inner.replace_value(id, record).await
226    }
227
228    async fn patch_value(
229        &self,
230        id: &Self::Id,
231        partial: &Record<Self::Value>,
232    ) -> Result<Record<Self::Value>> {
233        self.inner.patch_value(id, partial).await
234    }
235
236    async fn delete(&self, id: &Self::Id) -> Result<()> {
237        self.inner.delete(id).await
238    }
239
240    async fn delete_all(&self) -> Result<()> {
241        self.inner.delete_all().await
242    }
243}
244
245// Implement TableLike by delegating to inner
246#[async_trait]
247impl TableLike for AnyTable {
248    fn table_name(&self) -> &str {
249        self.inner.table_name()
250    }
251
252    fn table_alias(&self) -> &str {
253        self.inner.table_alias()
254    }
255
256    fn column_names(&self) -> Vec<String> {
257        self.inner.column_names()
258    }
259
260    fn id_field_name(&self) -> Option<String> {
261        self.inner.id_field_name()
262    }
263
264    fn title_field_names(&self) -> Vec<String> {
265        self.inner.title_field_names()
266    }
267
268    fn column_types(&self) -> IndexMap<String, &'static str> {
269        self.inner.column_types()
270    }
271
272    fn get_ref_names(&self) -> Vec<String> {
273        self.inner.get_ref_names()
274    }
275
276    fn add_condition(&mut self, condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
277        self.inner.add_condition(condition)
278    }
279
280    fn add_condition_eq(&mut self, field: &str, value: &str) -> Result<()> {
281        self.inner.add_condition_eq(field, value)
282    }
283
284    fn temp_add_condition(
285        &mut self,
286        condition: vantage_expressions::AnyExpression,
287    ) -> Result<ConditionHandle> {
288        self.inner.temp_add_condition(condition)
289    }
290
291    fn temp_remove_condition(&mut self, handle: ConditionHandle) -> Result<()> {
292        self.inner.temp_remove_condition(handle)
293    }
294
295    fn search_expression(&self, search_value: &str) -> Result<vantage_expressions::AnyExpression> {
296        self.inner.search_expression(search_value)
297    }
298
299    fn clone_box(&self) -> Box<dyn TableLike<Value = Self::Value, Id = Self::Id>> {
300        Box::new(self.clone())
301    }
302
303    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
304        self
305    }
306
307    fn as_any_ref(&self) -> &dyn std::any::Any {
308        self
309    }
310
311    fn set_pagination(&mut self, pagination: Option<Pagination>) {
312        self.inner.set_pagination(pagination)
313    }
314
315    fn get_pagination(&self) -> Option<&Pagination> {
316        self.inner.get_pagination()
317    }
318
319    async fn get_count(&self) -> vantage_core::Result<i64> {
320        self.inner.get_count().await
321    }
322
323    fn get_ref(&self, relation: &str) -> Result<AnyTable> {
324        self.inner.get_ref(relation)
325    }
326}
327
328impl AnyTable {
329    /// Traverse a named reference and return the related table as `AnyTable`.
330    ///
331    /// Inherent wrapper so callers don't need `use TableLike` in scope.
332    pub fn get_ref(&self, relation: &str) -> Result<AnyTable> {
333        TableLike::get_ref(self, relation)
334    }
335
336    /// Configure pagination using a callback
337    pub fn with_pagination<F>(&mut self, func: F)
338    where
339        F: FnOnce(&mut Pagination),
340    {
341        let mut pagination = self.inner.get_pagination().copied().unwrap_or_default();
342        func(&mut pagination);
343        self.inner.set_pagination(Some(pagination));
344    }
345}
346
347// ── CborAdapter: blanket bridge for non-CBOR table types ────────────────
348
349/// Wraps a `Table<T, E>` whose value/id types are not `ciborium::Value`/`String`,
350/// converting on the fly so it can satisfy `TableLike<Value = CborValue, Id = String>`.
351struct CborAdapter<T: TableSource, E: Entity<T::Value>> {
352    inner: Table<T, E>,
353}
354
355impl<T: TableSource, E: Entity<T::Value>> Clone for CborAdapter<T, E> {
356    fn clone(&self) -> Self {
357        Self {
358            inner: self.inner.clone(),
359        }
360    }
361}
362
363impl<T, E> CborAdapter<T, E>
364where
365    T: TableSource,
366    T::Value: Into<CborValue>,
367    T::Id: Display,
368    E: Entity<T::Value>,
369{
370    fn convert_record(record: Record<T::Value>) -> Record<CborValue> {
371        record.into_iter().map(|(k, v)| (k, v.into())).collect()
372    }
373
374    fn convert_record_back(record: &Record<CborValue>) -> Record<T::Value>
375    where
376        T::Value: From<CborValue>,
377    {
378        record
379            .iter()
380            .map(|(k, v)| (k.clone(), T::Value::from(v.clone())))
381            .collect()
382    }
383}
384
385impl<T, E> ValueSet for CborAdapter<T, E>
386where
387    T: TableSource,
388    E: Entity<T::Value>,
389{
390    type Id = String;
391    type Value = CborValue;
392}
393
394#[async_trait]
395impl<T, E> ReadableValueSet for CborAdapter<T, E>
396where
397    T: TableSource,
398    T::Value: Into<CborValue>,
399    T::Id: Display + From<String>,
400    E: Entity<T::Value>,
401{
402    async fn list_values(&self) -> Result<IndexMap<String, Record<CborValue>>> {
403        let raw = self
404            .inner
405            .data_source()
406            .list_table_values(&self.inner)
407            .await?;
408        Ok(raw
409            .into_iter()
410            .map(|(id, rec)| (id.to_string(), Self::convert_record(rec)))
411            .collect())
412    }
413
414    async fn get_value(&self, id: &String) -> Result<Option<Record<CborValue>>> {
415        let native_id: T::Id = id.clone().into();
416        let rec = self
417            .inner
418            .data_source()
419            .get_table_value(&self.inner, &native_id)
420            .await?;
421        Ok(rec.map(Self::convert_record))
422    }
423
424    async fn get_some_value(&self) -> Result<Option<(String, Record<CborValue>)>> {
425        let result = self
426            .inner
427            .data_source()
428            .get_table_some_value(&self.inner)
429            .await?;
430        Ok(result.map(|(id, rec)| (id.to_string(), Self::convert_record(rec))))
431    }
432}
433
434#[async_trait]
435impl<T, E> WritableValueSet for CborAdapter<T, E>
436where
437    T: TableSource,
438    T::Value: Into<CborValue> + From<CborValue>,
439    T::Id: Display + From<String>,
440    E: Entity<T::Value>,
441{
442    async fn insert_value(
443        &self,
444        id: &String,
445        record: &Record<CborValue>,
446    ) -> Result<Record<CborValue>> {
447        let native_id: T::Id = id.clone().into();
448        let native_rec = Self::convert_record_back(record);
449        let returned = self
450            .inner
451            .data_source()
452            .insert_table_value(&self.inner, &native_id, &native_rec)
453            .await?;
454        Ok(Self::convert_record(returned))
455    }
456
457    async fn replace_value(
458        &self,
459        id: &String,
460        record: &Record<CborValue>,
461    ) -> Result<Record<CborValue>> {
462        let native_id: T::Id = id.clone().into();
463        let native_rec = Self::convert_record_back(record);
464        let returned = self
465            .inner
466            .data_source()
467            .replace_table_value(&self.inner, &native_id, &native_rec)
468            .await?;
469        Ok(Self::convert_record(returned))
470    }
471
472    async fn patch_value(
473        &self,
474        id: &String,
475        partial: &Record<CborValue>,
476    ) -> Result<Record<CborValue>> {
477        let native_id: T::Id = id.clone().into();
478        let native_rec = Self::convert_record_back(partial);
479        let returned = self
480            .inner
481            .data_source()
482            .patch_table_value(&self.inner, &native_id, &native_rec)
483            .await?;
484        Ok(Self::convert_record(returned))
485    }
486
487    async fn delete(&self, id: &String) -> Result<()> {
488        let native_id: T::Id = id.clone().into();
489        self.inner
490            .data_source()
491            .delete_table_value(&self.inner, &native_id)
492            .await
493    }
494
495    async fn delete_all(&self) -> Result<()> {
496        self.inner
497            .data_source()
498            .delete_table_all_values(&self.inner)
499            .await
500    }
501}
502
503#[async_trait]
504impl<T, E> TableLike for CborAdapter<T, E>
505where
506    T: TableSource + 'static,
507    T::Value: Into<CborValue> + From<CborValue>,
508    T::Id: Display + From<String>,
509    E: Entity<T::Value> + 'static,
510{
511    fn table_name(&self) -> &str {
512        self.inner.table_name()
513    }
514
515    fn table_alias(&self) -> &str {
516        self.inner.table_name()
517    }
518
519    fn column_names(&self) -> Vec<String> {
520        self.inner.columns().keys().cloned().collect()
521    }
522
523    fn id_field_name(&self) -> Option<String> {
524        TableLike::id_field_name(&self.inner)
525    }
526
527    fn title_field_names(&self) -> Vec<String> {
528        TableLike::title_field_names(&self.inner)
529    }
530
531    fn column_types(&self) -> IndexMap<String, &'static str> {
532        TableLike::column_types(&self.inner)
533    }
534
535    fn get_ref_names(&self) -> Vec<String> {
536        TableLike::get_ref_names(&self.inner)
537    }
538
539    fn add_condition(&mut self, _condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
540        Err(error!("add_condition not supported through CborAdapter"))
541    }
542
543    fn add_condition_eq(&mut self, field: &str, value: &str) -> Result<()> {
544        TableLike::add_condition_eq(&mut self.inner, field, value)
545    }
546
547    fn temp_add_condition(
548        &mut self,
549        _condition: vantage_expressions::AnyExpression,
550    ) -> Result<ConditionHandle> {
551        Err(error!(
552            "temp_add_condition not supported through CborAdapter"
553        ))
554    }
555
556    fn temp_remove_condition(&mut self, _handle: ConditionHandle) -> Result<()> {
557        Err(error!(
558            "temp_remove_condition not supported through CborAdapter"
559        ))
560    }
561
562    fn search_expression(&self, _search_value: &str) -> Result<vantage_expressions::AnyExpression> {
563        Err(error!(
564            "search_expression not supported through CborAdapter"
565        ))
566    }
567
568    fn clone_box(&self) -> Box<dyn TableLike<Value = CborValue, Id = String>> {
569        Box::new(self.clone())
570    }
571
572    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
573        self
574    }
575
576    fn as_any_ref(&self) -> &dyn std::any::Any {
577        self
578    }
579
580    fn set_pagination(&mut self, pagination: Option<Pagination>) {
581        self.inner.set_pagination(pagination);
582    }
583
584    fn get_pagination(&self) -> Option<&Pagination> {
585        self.inner.pagination()
586    }
587
588    async fn get_count(&self) -> Result<i64> {
589        self.inner.data_source().get_table_count(&self.inner).await
590    }
591
592    fn get_ref(&self, relation: &str) -> Result<AnyTable> {
593        self.inner.get_ref(relation)
594    }
595}
596
597// Inline AnyTable tests using `MockTableSource` were dropped as part of the
598// CBOR swap. `MockTableSource` still uses `serde_json::Value`, but `AnyTable`
599// now requires `Value = CborValue`. A new CBOR-flavoured mock + restored
600// integration tests will live in `tests/table_like.rs` once `MockTableSource`
601// (or a sibling mock) gains CBOR support — see TODO.md "Architecture: Make
602// ImTable / ImDataSource generic over Value".