Skip to main content

vantage_api_client/
table_source.rs

1use async_trait::async_trait;
2use ciborium::Value as CborValue;
3use indexmap::IndexMap;
4use vantage_core::error;
5use vantage_dataset::traits::{ReadableValueSet, Result};
6use vantage_expressions::Expression;
7use vantage_expressions::traits::associated_expressions::AssociatedExpression;
8use vantage_expressions::traits::datasource::DataSource;
9use vantage_expressions::traits::expressive::ExpressiveEnum;
10use vantage_table::column::core::{Column, ColumnType};
11use vantage_table::table::Table;
12use vantage_table::traits::table_source::TableSource;
13use vantage_types::{Entity, Record};
14
15use vantage_expressions::traits::datasource::ExprDataSource;
16use vantage_expressions::traits::expressive::DeferredFn;
17
18use crate::RestApi;
19
20/// Extract the id field name from a table's column flags.
21fn id_field_name<E: Entity<CborValue>>(table: &Table<RestApi, E>) -> Option<String> {
22    table.id_field().map(|col| col.name().to_string())
23}
24
25impl ExprDataSource<CborValue> for RestApi {
26    async fn execute(&self, expr: &Expression<CborValue>) -> vantage_core::Result<CborValue> {
27        if expr.parameters.is_empty() {
28            Ok(CborValue::Text(expr.template.clone()))
29        } else {
30            Ok(CborValue::Null)
31        }
32    }
33
34    fn defer(&self, expr: Expression<CborValue>) -> DeferredFn<CborValue> {
35        let api = self.clone();
36        DeferredFn::new(move || {
37            let api = api.clone();
38            let expr = expr.clone();
39            Box::pin(async move {
40                let result = api.execute(&expr).await?;
41                Ok(ExpressiveEnum::Scalar(result))
42            })
43        })
44    }
45}
46
47impl DataSource for RestApi {}
48
49#[async_trait]
50impl TableSource for RestApi {
51    type Column<Type>
52        = Column<Type>
53    where
54        Type: ColumnType;
55    type AnyType = CborValue;
56    type Value = CborValue;
57    type Id = String;
58    type Condition = vantage_expressions::Expression<Self::Value>;
59
60    /// Build a stringy `field == value` eq-condition. Stringy callers
61    /// (the model-driven CLI) only have text on hand; values arrive as
62    /// `String` and become CBOR text scalars here. The peeling code
63    /// in `condition_to_query_param` renders all scalar variants the
64    /// same way so the URL still reads correctly.
65    fn eq_condition(field: &str, value: &str) -> Result<Self::Condition> {
66        Ok(crate::eq_condition(field, value.to_string()))
67    }
68
69    fn create_column<Type: ColumnType>(&self, name: &str) -> Self::Column<Type> {
70        Column::new(name)
71    }
72
73    fn to_any_column<Type: ColumnType>(
74        &self,
75        column: Self::Column<Type>,
76    ) -> Self::Column<Self::AnyType> {
77        Column::from_column(column)
78    }
79
80    fn convert_any_column<Type: ColumnType>(
81        &self,
82        any_column: Self::Column<Self::AnyType>,
83    ) -> Option<Self::Column<Type>> {
84        Some(Column::from_column(any_column))
85    }
86
87    fn expr(
88        &self,
89        template: impl Into<String>,
90        parameters: Vec<ExpressiveEnum<Self::Value>>,
91    ) -> Expression<Self::Value> {
92        Expression::new(template, parameters)
93    }
94
95    fn search_table_condition<E>(
96        &self,
97        _table: &Table<Self, E>,
98        search_value: &str,
99    ) -> Expression<Self::Value>
100    where
101        E: Entity<Self::Value>,
102    {
103        Expression::new(format!("SEARCH '{}'", search_value), vec![])
104    }
105
106    async fn list_table_values<E>(
107        &self,
108        table: &Table<Self, E>,
109    ) -> Result<IndexMap<Self::Id, Record<Self::Value>>>
110    where
111        E: Entity<Self::Value>,
112        Self: Sized,
113    {
114        self.fetch_records(
115            table.table_name(),
116            id_field_name(table).as_deref(),
117            table.pagination(),
118            table.conditions(),
119        )
120        .await
121    }
122
123    async fn get_table_value<E>(
124        &self,
125        table: &Table<Self, E>,
126        id: &Self::Id,
127    ) -> Result<Option<Record<Self::Value>>>
128    where
129        E: Entity<Self::Value>,
130        Self: Sized,
131    {
132        let records = self
133            .fetch_records(
134                table.table_name(),
135                id_field_name(table).as_deref(),
136                table.pagination(),
137                table.conditions(),
138            )
139            .await?;
140        Ok(records.get(id).cloned())
141    }
142
143    async fn get_table_some_value<E>(
144        &self,
145        table: &Table<Self, E>,
146    ) -> Result<Option<(Self::Id, Record<Self::Value>)>>
147    where
148        E: Entity<Self::Value>,
149        Self: Sized,
150    {
151        let records = self
152            .fetch_records(
153                table.table_name(),
154                id_field_name(table).as_deref(),
155                table.pagination(),
156                table.conditions(),
157            )
158            .await?;
159        Ok(records.into_iter().next())
160    }
161
162    async fn get_table_count<E>(&self, table: &Table<Self, E>) -> Result<i64>
163    where
164        E: Entity<Self::Value>,
165        Self: Sized,
166    {
167        let records = self
168            .fetch_records(
169                table.table_name(),
170                id_field_name(table).as_deref(),
171                table.pagination(),
172                table.conditions(),
173            )
174            .await?;
175        Ok(records.len() as i64)
176    }
177
178    async fn get_table_sum<E>(
179        &self,
180        _table: &Table<Self, E>,
181        _column: &Self::Column<Self::AnyType>,
182    ) -> Result<Self::Value>
183    where
184        E: Entity<Self::Value>,
185        Self: Sized,
186    {
187        Err(error!("Sum not implemented for API backend"))
188    }
189
190    async fn get_table_max<E>(
191        &self,
192        _table: &Table<Self, E>,
193        _column: &Self::Column<Self::AnyType>,
194    ) -> Result<Self::Value>
195    where
196        E: Entity<Self::Value>,
197        Self: Sized,
198    {
199        Err(error!("Max not implemented for API backend"))
200    }
201
202    async fn get_table_min<E>(
203        &self,
204        _table: &Table<Self, E>,
205        _column: &Self::Column<Self::AnyType>,
206    ) -> Result<Self::Value>
207    where
208        E: Entity<Self::Value>,
209        Self: Sized,
210    {
211        Err(error!("Min not implemented for API backend"))
212    }
213
214    async fn insert_table_value<E>(
215        &self,
216        _table: &Table<Self, E>,
217        _id: &Self::Id,
218        _record: &Record<Self::Value>,
219    ) -> Result<Record<Self::Value>>
220    where
221        E: Entity<Self::Value>,
222        Self: Sized,
223    {
224        Err(error!("REST API is a read-only data source"))
225    }
226
227    async fn replace_table_value<E>(
228        &self,
229        _table: &Table<Self, E>,
230        _id: &Self::Id,
231        _record: &Record<Self::Value>,
232    ) -> Result<Record<Self::Value>>
233    where
234        E: Entity<Self::Value>,
235        Self: Sized,
236    {
237        Err(error!("REST API is a read-only data source"))
238    }
239
240    async fn patch_table_value<E>(
241        &self,
242        _table: &Table<Self, E>,
243        _id: &Self::Id,
244        _partial: &Record<Self::Value>,
245    ) -> Result<Record<Self::Value>>
246    where
247        E: Entity<Self::Value>,
248        Self: Sized,
249    {
250        Err(error!("REST API is a read-only data source"))
251    }
252
253    async fn delete_table_value<E>(&self, _table: &Table<Self, E>, _id: &Self::Id) -> Result<()>
254    where
255        E: Entity<Self::Value>,
256        Self: Sized,
257    {
258        Err(error!("REST API is a read-only data source"))
259    }
260
261    async fn delete_table_all_values<E>(&self, _table: &Table<Self, E>) -> Result<()>
262    where
263        E: Entity<Self::Value>,
264        Self: Sized,
265    {
266        Err(error!("REST API is a read-only data source"))
267    }
268
269    async fn insert_table_return_id_value<E>(
270        &self,
271        _table: &Table<Self, E>,
272        _record: &Record<Self::Value>,
273    ) -> Result<Self::Id>
274    where
275        E: Entity<Self::Value>,
276        Self: Sized,
277    {
278        Err(error!("REST API is a read-only data source"))
279    }
280
281    /// Build a child condition for `with_many` / `with_one` traversal.
282    ///
283    /// REST APIs can't run subqueries, so this resolves on two paths:
284    ///
285    /// * **Sync peek** — if the parent already carries an eq-condition
286    ///   on `source_column`, we re-key its value onto `target_field`
287    ///   and we're done. Covers the common `with_many` case where
288    ///   `source_column` is the parent's id field (narrowed via
289    ///   `id=N` or `[N]`).
290    /// * **Deferred read** — otherwise (the `with_one` case, where
291    ///   `source_column` is a foreign-key field that lives in the
292    ///   parent's record, not its conditions), we wrap the resolution
293    ///   in a `DeferredFn` that fetches the parent at request time and
294    ///   pulls the field out of the row. `fetch_records` resolves
295    ///   deferreds before peeling conditions into query params.
296    fn related_in_condition<SourceE: Entity<Self::Value> + 'static>(
297        &self,
298        target_field: &str,
299        source_table: &Table<Self, SourceE>,
300        source_column: &str,
301    ) -> Self::Condition
302    where
303        Self: Sized,
304    {
305        for cond in source_table.conditions() {
306            if let Some((field, value)) = crate::condition_to_query_param(cond)
307                && field == source_column
308            {
309                let cbor_value: CborValue = if let Ok(i) = value.parse::<i64>() {
310                    CborValue::Integer(i.into())
311                } else if let Ok(f) = value.parse::<f64>() {
312                    CborValue::Float(f)
313                } else {
314                    CborValue::Text(value)
315                };
316                return crate::eq_condition(target_field, cbor_value);
317            }
318        }
319
320        // Deferred fallback. Clone the parent table into the closure
321        // so it stays valid past this stack frame; at fetch time we
322        // list its values, take the first, and pull `source_column`.
323        let parent = source_table.clone();
324        let column = source_column.to_string();
325        let parent_name = source_table.table_name().to_string();
326        let deferred = DeferredFn::new(move || {
327            let parent = parent.clone();
328            let column = column.clone();
329            let parent_name = parent_name.clone();
330            Box::pin(async move {
331                let records = parent.list_values().await?;
332                let value = records
333                    .values()
334                    .next()
335                    .and_then(|r| r.get(&column))
336                    .cloned()
337                    .ok_or_else(|| {
338                        error!(
339                            "Deferred FK resolve: parent yielded no row or column missing",
340                            table = parent_name,
341                            column = column
342                        )
343                    })?;
344                Ok(ExpressiveEnum::Scalar(value))
345            })
346        });
347
348        Expression::new(
349            "{} = {}",
350            vec![
351                ExpressiveEnum::Nested(Expression::new(target_field.to_string(), vec![])),
352                ExpressiveEnum::Nested(Expression::new(
353                    "{}",
354                    vec![ExpressiveEnum::Deferred(deferred)],
355                )),
356            ],
357        )
358    }
359
360    fn column_table_values_expr<'a, E, Type: ColumnType>(
361        &'a self,
362        _table: &Table<Self, E>,
363        _column: &Self::Column<Type>,
364    ) -> AssociatedExpression<'a, Self, Self::Value, Vec<Type>>
365    where
366        E: Entity<Self::Value> + 'static,
367        Self: Sized,
368    {
369        unimplemented!("column_table_values_expr not yet supported for REST API")
370    }
371}