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 add_condition(&mut self, condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
261        self.inner.add_condition(condition)
262    }
263
264    fn temp_add_condition(
265        &mut self,
266        condition: vantage_expressions::AnyExpression,
267    ) -> Result<ConditionHandle> {
268        self.inner.temp_add_condition(condition)
269    }
270
271    fn temp_remove_condition(&mut self, handle: ConditionHandle) -> Result<()> {
272        self.inner.temp_remove_condition(handle)
273    }
274
275    fn search_expression(&self, search_value: &str) -> Result<vantage_expressions::AnyExpression> {
276        self.inner.search_expression(search_value)
277    }
278
279    fn clone_box(&self) -> Box<dyn TableLike<Value = Self::Value, Id = Self::Id>> {
280        Box::new(self.clone())
281    }
282
283    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
284        self
285    }
286
287    fn as_any_ref(&self) -> &dyn std::any::Any {
288        self
289    }
290
291    fn set_pagination(&mut self, pagination: Option<Pagination>) {
292        self.inner.set_pagination(pagination)
293    }
294
295    fn get_pagination(&self) -> Option<&Pagination> {
296        self.inner.get_pagination()
297    }
298
299    async fn get_count(&self) -> vantage_core::Result<i64> {
300        self.inner.get_count().await
301    }
302}
303
304impl AnyTable {
305    /// Configure pagination using a callback
306    pub fn with_pagination<F>(&mut self, func: F)
307    where
308        F: FnOnce(&mut Pagination),
309    {
310        let mut pagination = self.inner.get_pagination().copied().unwrap_or_default();
311        func(&mut pagination);
312        self.inner.set_pagination(Some(pagination));
313    }
314}
315
316// ── CborAdapter: blanket bridge for non-CBOR table types ────────────────
317
318/// Wraps a `Table<T, E>` whose value/id types are not `ciborium::Value`/`String`,
319/// converting on the fly so it can satisfy `TableLike<Value = CborValue, Id = String>`.
320struct CborAdapter<T: TableSource, E: Entity<T::Value>> {
321    inner: Table<T, E>,
322}
323
324impl<T: TableSource, E: Entity<T::Value>> Clone for CborAdapter<T, E> {
325    fn clone(&self) -> Self {
326        Self {
327            inner: self.inner.clone(),
328        }
329    }
330}
331
332impl<T, E> CborAdapter<T, E>
333where
334    T: TableSource,
335    T::Value: Into<CborValue>,
336    T::Id: Display,
337    E: Entity<T::Value>,
338{
339    fn convert_record(record: Record<T::Value>) -> Record<CborValue> {
340        record.into_iter().map(|(k, v)| (k, v.into())).collect()
341    }
342
343    fn convert_record_back(record: &Record<CborValue>) -> Record<T::Value>
344    where
345        T::Value: From<CborValue>,
346    {
347        record
348            .iter()
349            .map(|(k, v)| (k.clone(), T::Value::from(v.clone())))
350            .collect()
351    }
352}
353
354impl<T, E> ValueSet for CborAdapter<T, E>
355where
356    T: TableSource,
357    E: Entity<T::Value>,
358{
359    type Id = String;
360    type Value = CborValue;
361}
362
363#[async_trait]
364impl<T, E> ReadableValueSet for CborAdapter<T, E>
365where
366    T: TableSource,
367    T::Value: Into<CborValue>,
368    T::Id: Display + From<String>,
369    E: Entity<T::Value>,
370{
371    async fn list_values(&self) -> Result<IndexMap<String, Record<CborValue>>> {
372        let raw = self
373            .inner
374            .data_source()
375            .list_table_values(&self.inner)
376            .await?;
377        Ok(raw
378            .into_iter()
379            .map(|(id, rec)| (id.to_string(), Self::convert_record(rec)))
380            .collect())
381    }
382
383    async fn get_value(&self, id: &String) -> Result<Option<Record<CborValue>>> {
384        let native_id: T::Id = id.clone().into();
385        let rec = self
386            .inner
387            .data_source()
388            .get_table_value(&self.inner, &native_id)
389            .await?;
390        Ok(rec.map(Self::convert_record))
391    }
392
393    async fn get_some_value(&self) -> Result<Option<(String, Record<CborValue>)>> {
394        let result = self
395            .inner
396            .data_source()
397            .get_table_some_value(&self.inner)
398            .await?;
399        Ok(result.map(|(id, rec)| (id.to_string(), Self::convert_record(rec))))
400    }
401}
402
403#[async_trait]
404impl<T, E> WritableValueSet for CborAdapter<T, E>
405where
406    T: TableSource,
407    T::Value: Into<CborValue> + From<CborValue>,
408    T::Id: Display + From<String>,
409    E: Entity<T::Value>,
410{
411    async fn insert_value(
412        &self,
413        id: &String,
414        record: &Record<CborValue>,
415    ) -> Result<Record<CborValue>> {
416        let native_id: T::Id = id.clone().into();
417        let native_rec = Self::convert_record_back(record);
418        let returned = self
419            .inner
420            .data_source()
421            .insert_table_value(&self.inner, &native_id, &native_rec)
422            .await?;
423        Ok(Self::convert_record(returned))
424    }
425
426    async fn replace_value(
427        &self,
428        id: &String,
429        record: &Record<CborValue>,
430    ) -> Result<Record<CborValue>> {
431        let native_id: T::Id = id.clone().into();
432        let native_rec = Self::convert_record_back(record);
433        let returned = self
434            .inner
435            .data_source()
436            .replace_table_value(&self.inner, &native_id, &native_rec)
437            .await?;
438        Ok(Self::convert_record(returned))
439    }
440
441    async fn patch_value(
442        &self,
443        id: &String,
444        partial: &Record<CborValue>,
445    ) -> Result<Record<CborValue>> {
446        let native_id: T::Id = id.clone().into();
447        let native_rec = Self::convert_record_back(partial);
448        let returned = self
449            .inner
450            .data_source()
451            .patch_table_value(&self.inner, &native_id, &native_rec)
452            .await?;
453        Ok(Self::convert_record(returned))
454    }
455
456    async fn delete(&self, id: &String) -> Result<()> {
457        let native_id: T::Id = id.clone().into();
458        self.inner
459            .data_source()
460            .delete_table_value(&self.inner, &native_id)
461            .await
462    }
463
464    async fn delete_all(&self) -> Result<()> {
465        self.inner
466            .data_source()
467            .delete_table_all_values(&self.inner)
468            .await
469    }
470}
471
472#[async_trait]
473impl<T, E> TableLike for CborAdapter<T, E>
474where
475    T: TableSource + 'static,
476    T::Value: Into<CborValue> + From<CborValue>,
477    T::Id: Display + From<String>,
478    E: Entity<T::Value> + 'static,
479{
480    fn table_name(&self) -> &str {
481        self.inner.table_name()
482    }
483
484    fn table_alias(&self) -> &str {
485        self.inner.table_name()
486    }
487
488    fn column_names(&self) -> Vec<String> {
489        self.inner.columns().keys().cloned().collect()
490    }
491
492    fn add_condition(&mut self, _condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
493        Err(error!("add_condition not supported through CborAdapter"))
494    }
495
496    fn temp_add_condition(
497        &mut self,
498        _condition: vantage_expressions::AnyExpression,
499    ) -> Result<ConditionHandle> {
500        Err(error!(
501            "temp_add_condition not supported through CborAdapter"
502        ))
503    }
504
505    fn temp_remove_condition(&mut self, _handle: ConditionHandle) -> Result<()> {
506        Err(error!(
507            "temp_remove_condition not supported through CborAdapter"
508        ))
509    }
510
511    fn search_expression(&self, _search_value: &str) -> Result<vantage_expressions::AnyExpression> {
512        Err(error!(
513            "search_expression not supported through CborAdapter"
514        ))
515    }
516
517    fn clone_box(&self) -> Box<dyn TableLike<Value = CborValue, Id = String>> {
518        Box::new(self.clone())
519    }
520
521    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
522        self
523    }
524
525    fn as_any_ref(&self) -> &dyn std::any::Any {
526        self
527    }
528
529    fn set_pagination(&mut self, pagination: Option<Pagination>) {
530        self.inner.set_pagination(pagination);
531    }
532
533    fn get_pagination(&self) -> Option<&Pagination> {
534        self.inner.pagination()
535    }
536
537    async fn get_count(&self) -> Result<i64> {
538        self.inner.data_source().get_table_count(&self.inner).await
539    }
540}
541
542// Inline AnyTable tests using `MockTableSource` were dropped as part of the
543// CBOR swap. `MockTableSource` still uses `serde_json::Value`, but `AnyTable`
544// now requires `Value = CborValue`. A new CBOR-flavoured mock + restored
545// integration tests will live in `tests/table_like.rs` once `MockTableSource`
546// (or a sibling mock) gains CBOR support — see TODO.md "Architecture: Make
547// ImTable / ImDataSource generic over Value".