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