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    /// Attempt to downcast to a concrete `Table<T, E>`
88    ///
89    /// Returns `Err(self)` if the type doesn't match, allowing recovery
90    pub fn downcast<
91        T: TableSource<Value = CborValue, Id = String> + 'static,
92        E: Entity<CborValue> + 'static,
93    >(
94        self,
95    ) -> Result<Table<T, E>> {
96        // Check TypeIds for better error messages
97        if self.datasource_type_id != TypeId::of::<T>() {
98            let expected = std::any::type_name::<T>();
99            return Err(error!(
100                "DataSource type mismatch",
101                expected = expected,
102                actual = self.datasource_name
103            ));
104        }
105        if self.entity_type_id != TypeId::of::<E>() {
106            let expected = std::any::type_name::<E>();
107            return Err(error!(
108                "Entity type mismatch",
109                expected = expected,
110                actual = self.entity_name
111            ));
112        }
113
114        // Perform the actual downcast
115        self.inner
116            .into_any()
117            .downcast::<Table<T, E>>()
118            .map(|boxed| *boxed)
119            .map_err(|_| error!("Failed to downcast table"))
120    }
121
122    /// Get the datasource type name for debugging
123    pub fn datasource_name(&self) -> &str {
124        self.datasource_name
125    }
126
127    /// Get the entity type name for debugging
128    pub fn entity_name(&self) -> &str {
129        self.entity_name
130    }
131
132    /// Get the datasource TypeId
133    pub fn datasource_type_id(&self) -> TypeId {
134        self.datasource_type_id
135    }
136
137    /// Get the entity TypeId
138    pub fn entity_type_id(&self) -> TypeId {
139        self.entity_type_id
140    }
141
142    /// Check if this table matches the given types
143    pub fn is_type<T: TableSource + 'static, E: Entity<CborValue> + 'static>(&self) -> bool {
144        self.datasource_type_id == TypeId::of::<T>() && self.entity_type_id == TypeId::of::<E>()
145    }
146}
147
148impl Clone for AnyTable {
149    fn clone(&self) -> Self {
150        Self {
151            inner: self.inner.clone_box(),
152            datasource_type_id: self.datasource_type_id,
153            entity_type_id: self.entity_type_id,
154            datasource_name: self.datasource_name,
155            entity_name: self.entity_name,
156        }
157    }
158}
159
160impl std::fmt::Debug for AnyTable {
161    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
162        f.debug_struct("AnyTable")
163            .field("datasource", &self.datasource_name)
164            .field("entity", &self.entity_name)
165            .finish()
166    }
167}
168
169// Implement ValueSet first
170impl ValueSet for AnyTable {
171    type Id = String;
172    type Value = CborValue;
173}
174
175// Implement ReadableValueSet by delegating to inner TableLike
176#[async_trait]
177impl ReadableValueSet for AnyTable {
178    async fn list_values(&self) -> Result<IndexMap<Self::Id, Record<Self::Value>>> {
179        self.inner.list_values().await
180    }
181
182    async fn get_value(&self, id: &Self::Id) -> Result<Option<Record<Self::Value>>> {
183        self.inner.get_value(id).await
184    }
185
186    async fn get_some_value(&self) -> Result<Option<(Self::Id, Record<Self::Value>)>> {
187        self.inner.get_some_value().await
188    }
189}
190
191// Implement WritableValueSet by delegating to inner TableLike
192#[async_trait]
193impl WritableValueSet for AnyTable {
194    async fn insert_value(
195        &self,
196        id: &Self::Id,
197        record: &Record<Self::Value>,
198    ) -> Result<Record<Self::Value>> {
199        self.inner.insert_value(id, record).await
200    }
201
202    async fn replace_value(
203        &self,
204        id: &Self::Id,
205        record: &Record<Self::Value>,
206    ) -> Result<Record<Self::Value>> {
207        self.inner.replace_value(id, record).await
208    }
209
210    async fn patch_value(
211        &self,
212        id: &Self::Id,
213        partial: &Record<Self::Value>,
214    ) -> Result<Record<Self::Value>> {
215        self.inner.patch_value(id, partial).await
216    }
217
218    async fn delete(&self, id: &Self::Id) -> Result<()> {
219        self.inner.delete(id).await
220    }
221
222    async fn delete_all(&self) -> Result<()> {
223        self.inner.delete_all().await
224    }
225}
226
227// Implement TableLike by delegating to inner
228#[async_trait]
229impl TableLike for AnyTable {
230    fn table_name(&self) -> &str {
231        self.inner.table_name()
232    }
233
234    fn table_alias(&self) -> &str {
235        self.inner.table_alias()
236    }
237
238    fn column_names(&self) -> Vec<String> {
239        self.inner.column_names()
240    }
241
242    fn add_condition(&mut self, condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
243        self.inner.add_condition(condition)
244    }
245
246    fn temp_add_condition(
247        &mut self,
248        condition: vantage_expressions::AnyExpression,
249    ) -> Result<ConditionHandle> {
250        self.inner.temp_add_condition(condition)
251    }
252
253    fn temp_remove_condition(&mut self, handle: ConditionHandle) -> Result<()> {
254        self.inner.temp_remove_condition(handle)
255    }
256
257    fn search_expression(&self, search_value: &str) -> Result<vantage_expressions::AnyExpression> {
258        self.inner.search_expression(search_value)
259    }
260
261    fn clone_box(&self) -> Box<dyn TableLike<Value = Self::Value, Id = Self::Id>> {
262        Box::new(self.clone())
263    }
264
265    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
266        self
267    }
268
269    fn as_any_ref(&self) -> &dyn std::any::Any {
270        self
271    }
272
273    fn set_pagination(&mut self, pagination: Option<Pagination>) {
274        self.inner.set_pagination(pagination)
275    }
276
277    fn get_pagination(&self) -> Option<&Pagination> {
278        self.inner.get_pagination()
279    }
280
281    async fn get_count(&self) -> vantage_core::Result<i64> {
282        self.inner.get_count().await
283    }
284}
285
286impl AnyTable {
287    /// Configure pagination using a callback
288    pub fn with_pagination<F>(&mut self, func: F)
289    where
290        F: FnOnce(&mut Pagination),
291    {
292        let mut pagination = self.inner.get_pagination().copied().unwrap_or_default();
293        func(&mut pagination);
294        self.inner.set_pagination(Some(pagination));
295    }
296}
297
298// ── CborAdapter: blanket bridge for non-CBOR table types ────────────────
299
300/// Wraps a `Table<T, E>` whose value/id types are not `ciborium::Value`/`String`,
301/// converting on the fly so it can satisfy `TableLike<Value = CborValue, Id = String>`.
302struct CborAdapter<T: TableSource, E: Entity<T::Value>> {
303    inner: Table<T, E>,
304}
305
306impl<T: TableSource, E: Entity<T::Value>> Clone for CborAdapter<T, E> {
307    fn clone(&self) -> Self {
308        Self {
309            inner: self.inner.clone(),
310        }
311    }
312}
313
314impl<T, E> CborAdapter<T, E>
315where
316    T: TableSource,
317    T::Value: Into<CborValue>,
318    T::Id: Display,
319    E: Entity<T::Value>,
320{
321    fn convert_record(record: Record<T::Value>) -> Record<CborValue> {
322        record.into_iter().map(|(k, v)| (k, v.into())).collect()
323    }
324
325    fn convert_record_back(record: &Record<CborValue>) -> Record<T::Value>
326    where
327        T::Value: From<CborValue>,
328    {
329        record
330            .iter()
331            .map(|(k, v)| (k.clone(), T::Value::from(v.clone())))
332            .collect()
333    }
334}
335
336impl<T, E> ValueSet for CborAdapter<T, E>
337where
338    T: TableSource,
339    E: Entity<T::Value>,
340{
341    type Id = String;
342    type Value = CborValue;
343}
344
345#[async_trait]
346impl<T, E> ReadableValueSet for CborAdapter<T, E>
347where
348    T: TableSource,
349    T::Value: Into<CborValue>,
350    T::Id: Display + From<String>,
351    E: Entity<T::Value>,
352{
353    async fn list_values(&self) -> Result<IndexMap<String, Record<CborValue>>> {
354        let raw = self
355            .inner
356            .data_source()
357            .list_table_values(&self.inner)
358            .await?;
359        Ok(raw
360            .into_iter()
361            .map(|(id, rec)| (id.to_string(), Self::convert_record(rec)))
362            .collect())
363    }
364
365    async fn get_value(&self, id: &String) -> Result<Option<Record<CborValue>>> {
366        let native_id: T::Id = id.clone().into();
367        let rec = self
368            .inner
369            .data_source()
370            .get_table_value(&self.inner, &native_id)
371            .await?;
372        Ok(rec.map(Self::convert_record))
373    }
374
375    async fn get_some_value(&self) -> Result<Option<(String, Record<CborValue>)>> {
376        let result = self
377            .inner
378            .data_source()
379            .get_table_some_value(&self.inner)
380            .await?;
381        Ok(result.map(|(id, rec)| (id.to_string(), Self::convert_record(rec))))
382    }
383}
384
385#[async_trait]
386impl<T, E> WritableValueSet for CborAdapter<T, E>
387where
388    T: TableSource,
389    T::Value: Into<CborValue> + From<CborValue>,
390    T::Id: Display + From<String>,
391    E: Entity<T::Value>,
392{
393    async fn insert_value(
394        &self,
395        id: &String,
396        record: &Record<CborValue>,
397    ) -> Result<Record<CborValue>> {
398        let native_id: T::Id = id.clone().into();
399        let native_rec = Self::convert_record_back(record);
400        let returned = self
401            .inner
402            .data_source()
403            .insert_table_value(&self.inner, &native_id, &native_rec)
404            .await?;
405        Ok(Self::convert_record(returned))
406    }
407
408    async fn replace_value(
409        &self,
410        id: &String,
411        record: &Record<CborValue>,
412    ) -> Result<Record<CborValue>> {
413        let native_id: T::Id = id.clone().into();
414        let native_rec = Self::convert_record_back(record);
415        let returned = self
416            .inner
417            .data_source()
418            .replace_table_value(&self.inner, &native_id, &native_rec)
419            .await?;
420        Ok(Self::convert_record(returned))
421    }
422
423    async fn patch_value(
424        &self,
425        id: &String,
426        partial: &Record<CborValue>,
427    ) -> Result<Record<CborValue>> {
428        let native_id: T::Id = id.clone().into();
429        let native_rec = Self::convert_record_back(partial);
430        let returned = self
431            .inner
432            .data_source()
433            .patch_table_value(&self.inner, &native_id, &native_rec)
434            .await?;
435        Ok(Self::convert_record(returned))
436    }
437
438    async fn delete(&self, id: &String) -> Result<()> {
439        let native_id: T::Id = id.clone().into();
440        self.inner
441            .data_source()
442            .delete_table_value(&self.inner, &native_id)
443            .await
444    }
445
446    async fn delete_all(&self) -> Result<()> {
447        self.inner
448            .data_source()
449            .delete_table_all_values(&self.inner)
450            .await
451    }
452}
453
454#[async_trait]
455impl<T, E> TableLike for CborAdapter<T, E>
456where
457    T: TableSource + 'static,
458    T::Value: Into<CborValue> + From<CborValue>,
459    T::Id: Display + From<String>,
460    E: Entity<T::Value> + 'static,
461{
462    fn table_name(&self) -> &str {
463        self.inner.table_name()
464    }
465
466    fn table_alias(&self) -> &str {
467        self.inner.table_name()
468    }
469
470    fn column_names(&self) -> Vec<String> {
471        self.inner.columns().keys().cloned().collect()
472    }
473
474    fn add_condition(&mut self, _condition: Box<dyn std::any::Any + Send + Sync>) -> Result<()> {
475        Err(error!("add_condition not supported through CborAdapter"))
476    }
477
478    fn temp_add_condition(
479        &mut self,
480        _condition: vantage_expressions::AnyExpression,
481    ) -> Result<ConditionHandle> {
482        Err(error!(
483            "temp_add_condition not supported through CborAdapter"
484        ))
485    }
486
487    fn temp_remove_condition(&mut self, _handle: ConditionHandle) -> Result<()> {
488        Err(error!(
489            "temp_remove_condition not supported through CborAdapter"
490        ))
491    }
492
493    fn search_expression(&self, _search_value: &str) -> Result<vantage_expressions::AnyExpression> {
494        Err(error!(
495            "search_expression not supported through CborAdapter"
496        ))
497    }
498
499    fn clone_box(&self) -> Box<dyn TableLike<Value = CborValue, Id = String>> {
500        Box::new(self.clone())
501    }
502
503    fn into_any(self: Box<Self>) -> Box<dyn std::any::Any> {
504        self
505    }
506
507    fn as_any_ref(&self) -> &dyn std::any::Any {
508        self
509    }
510
511    fn set_pagination(&mut self, pagination: Option<Pagination>) {
512        self.inner.set_pagination(pagination);
513    }
514
515    fn get_pagination(&self) -> Option<&Pagination> {
516        self.inner.pagination()
517    }
518
519    async fn get_count(&self) -> Result<i64> {
520        self.inner.data_source().get_table_count(&self.inner).await
521    }
522}
523
524// Inline AnyTable tests using `MockTableSource` were dropped as part of the
525// CBOR swap. `MockTableSource` still uses `serde_json::Value`, but `AnyTable`
526// now requires `Value = CborValue`. A new CBOR-flavoured mock + restored
527// integration tests will live in `tests/table_like.rs` once `MockTableSource`
528// (or a sibling mock) gains CBOR support — see TODO.md "Architecture: Make
529// ImTable / ImDataSource generic over Value".