Skip to main content

vantage_cmd/
table_source.rs

1//! `TableSource` impl for `Cmd`.
2//!
3//! The one interesting read is the list (`list.rs`): it resolves any
4//! deferred (relation) conditions, hands the read context to the Rhai
5//! script on a blocking thread (which builds the argv, runs the locked
6//! command, and parses the output), then keys the resulting rows by their
7//! id field and applies a client-side `Eq`/`In` filter as a safety net. A
8//! get by id goes through the table's DETAIL script when it has one
9//! (`detail.rs`).
10//!
11//! Read-only: writes and aggregations error, exactly like `vantage-aws`.
12
13use async_trait::async_trait;
14use ciborium::Value as CborValue;
15use indexmap::IndexMap;
16use serde_json::json;
17
18use vantage_core::error;
19use vantage_dataset::traits::Result;
20use vantage_expressions::{
21    Expression, Expressive, expr_any,
22    traits::associated_expressions::AssociatedExpression,
23    traits::datasource::ExprDataSource,
24    traits::expressive::{DeferredFn, ExpressiveEnum},
25};
26use vantage_table::column::core::{Column, ColumnType};
27use vantage_table::table::Table;
28use vantage_table::traits::table_source::TableSource;
29use vantage_types::{Entity, Record};
30
31use crate::cmd::Cmd;
32use crate::condition::CmdCondition;
33
34mod detail;
35mod list;
36
37#[async_trait]
38impl TableSource for Cmd {
39    type Column<Type>
40        = Column<Type>
41    where
42        Type: ColumnType;
43    type AnyType = CborValue;
44    type Value = CborValue;
45    type Id = String;
46    type Condition = CmdCondition;
47    type Source = String;
48
49    fn eq_condition(field: &str, value: &str) -> Result<Self::Condition> {
50        Ok(CmdCondition::eq(field.to_string(), value.to_string()))
51    }
52
53    fn eq_value_condition(&self, field: &str, value: Self::Value) -> Result<Self::Condition> {
54        Ok(CmdCondition::eq(field.to_string(), value))
55    }
56
57    fn create_column<Type: ColumnType>(&self, name: &str) -> Self::Column<Type> {
58        Column::new(name)
59    }
60
61    fn to_any_column<Type: ColumnType>(
62        &self,
63        column: Self::Column<Type>,
64    ) -> Self::Column<Self::AnyType> {
65        Column::from_column(column)
66    }
67
68    fn convert_any_column<Type: ColumnType>(
69        &self,
70        any_column: Self::Column<Self::AnyType>,
71    ) -> Option<Self::Column<Type>> {
72        Some(Column::from_column(any_column))
73    }
74
75    fn expr(
76        &self,
77        template: impl Into<String>,
78        parameters: Vec<ExpressiveEnum<Self::Value>>,
79    ) -> Expression<Self::Value> {
80        Expression::new(template, parameters)
81    }
82
83    fn search_table_condition<E>(
84        &self,
85        _table: &Table<Self, E>,
86        search_value: &str,
87    ) -> Self::Condition
88    where
89        E: Entity<Self::Value>,
90    {
91        // The script decides how to use a `__search__` condition (e.g.
92        // CloudWatch's `--filter-pattern`); there's no generic field set
93        // at this layer.
94        CmdCondition::eq("__search__", json!(search_value).to_string())
95    }
96
97    async fn list_table_values<E>(
98        &self,
99        table: &Table<Self, E>,
100    ) -> Result<IndexMap<Self::Id, Record<Self::Value>>>
101    where
102        E: Entity<Self::Value>,
103        Self: Sized,
104    {
105        self.list_rows(table).await
106    }
107
108    async fn get_table_value<E>(
109        &self,
110        table: &Table<Self, E>,
111        id: &Self::Id,
112    ) -> Result<Option<Record<Self::Value>>>
113    where
114        E: Entity<Self::Value>,
115        Self: Sized,
116    {
117        // Detail hydration with no caller-supplied row (id-only path).
118        let empty: Record<CborValue> = Record::new();
119        self.get_table_value_with_row(table, id, &empty).await
120    }
121
122    async fn get_table_some_value<E>(
123        &self,
124        table: &Table<Self, E>,
125    ) -> Result<Option<(Self::Id, Record<Self::Value>)>>
126    where
127        E: Entity<Self::Value>,
128        Self: Sized,
129    {
130        let all = self.list_table_values(table).await?;
131        Ok(all.into_iter().next())
132    }
133
134    async fn get_table_count<E>(&self, table: &Table<Self, E>) -> Result<i64>
135    where
136        E: Entity<Self::Value>,
137        Self: Sized,
138    {
139        let all = self.list_table_values(table).await?;
140        Ok(all.len() as i64)
141    }
142
143    async fn get_table_sum<E>(
144        &self,
145        _table: &Table<Self, E>,
146        _column: &Self::Column<Self::AnyType>,
147    ) -> Result<Self::Value>
148    where
149        E: Entity<Self::Value>,
150        Self: Sized,
151    {
152        Err(error!("Aggregations not supported by vantage-cmd"))
153    }
154
155    async fn get_table_max<E>(
156        &self,
157        _table: &Table<Self, E>,
158        _column: &Self::Column<Self::AnyType>,
159    ) -> Result<Self::Value>
160    where
161        E: Entity<Self::Value>,
162        Self: Sized,
163    {
164        Err(error!("Aggregations not supported by vantage-cmd"))
165    }
166
167    async fn get_table_min<E>(
168        &self,
169        _table: &Table<Self, E>,
170        _column: &Self::Column<Self::AnyType>,
171    ) -> Result<Self::Value>
172    where
173        E: Entity<Self::Value>,
174        Self: Sized,
175    {
176        Err(error!("Aggregations not supported by vantage-cmd"))
177    }
178
179    async fn insert_table_value<E>(
180        &self,
181        _table: &Table<Self, E>,
182        _id: &Self::Id,
183        _record: &Record<Self::Value>,
184    ) -> Result<Record<Self::Value>>
185    where
186        E: Entity<Self::Value>,
187        Self: Sized,
188    {
189        Err(error!("vantage-cmd is read-only"))
190    }
191
192    async fn replace_table_value<E>(
193        &self,
194        _table: &Table<Self, E>,
195        _id: &Self::Id,
196        _record: &Record<Self::Value>,
197    ) -> Result<Record<Self::Value>>
198    where
199        E: Entity<Self::Value>,
200        Self: Sized,
201    {
202        Err(error!("vantage-cmd is read-only"))
203    }
204
205    async fn patch_table_value<E>(
206        &self,
207        _table: &Table<Self, E>,
208        _id: &Self::Id,
209        _partial: &Record<Self::Value>,
210    ) -> Result<Record<Self::Value>>
211    where
212        E: Entity<Self::Value>,
213        Self: Sized,
214    {
215        Err(error!("vantage-cmd is read-only"))
216    }
217
218    async fn delete_table_value<E>(&self, _table: &Table<Self, E>, _id: &Self::Id) -> Result<()>
219    where
220        E: Entity<Self::Value>,
221        Self: Sized,
222    {
223        Err(error!("vantage-cmd is read-only"))
224    }
225
226    async fn delete_table_all_values<E>(&self, _table: &Table<Self, E>) -> Result<()>
227    where
228        E: Entity<Self::Value>,
229        Self: Sized,
230    {
231        Err(error!("vantage-cmd is read-only"))
232    }
233
234    async fn insert_table_return_id_value<E>(
235        &self,
236        _table: &Table<Self, E>,
237        _record: &Record<Self::Value>,
238    ) -> Result<Self::Id>
239    where
240        E: Entity<Self::Value>,
241        Self: Sized,
242    {
243        Err(error!("vantage-cmd is read-only"))
244    }
245
246    fn related_in_condition<SourceE: Entity<Self::Value> + 'static>(
247        &self,
248        target_field: &str,
249        source_table: &Table<Self, SourceE>,
250        source_column: &str,
251    ) -> Self::Condition
252    where
253        Self: Sized,
254    {
255        // "target_field IN (subquery)" as a Deferred condition, resolved
256        // at read time in `list_rows`. Same shape as vantage-aws.
257        let src_col = self.create_column::<Self::AnyType>(source_column);
258        let values_expr = self.column_table_values_expr(source_table, &src_col);
259        CmdCondition::Deferred {
260            field: target_field.to_string(),
261            source: values_expr.expr(),
262        }
263    }
264
265    fn column_table_values_expr<'a, E, Type: ColumnType>(
266        &'a self,
267        table: &Table<Self, E>,
268        column: &Self::Column<Type>,
269    ) -> AssociatedExpression<'a, Self, Self::Value, Vec<Type>>
270    where
271        E: Entity<Self::Value> + 'static,
272        Self: Sized,
273    {
274        let table_clone = table.clone();
275        let col = column.name().to_string();
276        let cmd = self.clone();
277
278        let inner = expr_any!("{}", {
279            DeferredFn::new(move || {
280                let cmd = cmd.clone();
281                let table = table_clone.clone();
282                let col = col.clone();
283                Box::pin(async move {
284                    let records = cmd.list_table_values(&table).await?;
285                    let values: Vec<CborValue> = records
286                        .values()
287                        .filter_map(|r| r.get(&col).cloned())
288                        .collect();
289                    Ok(ExpressiveEnum::Scalar(CborValue::Array(values)))
290                })
291            })
292        });
293
294        let expr = expr_any!("{}", { self.defer(inner) });
295        AssociatedExpression::new(expr, self)
296    }
297}