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