Skip to main content

vantage_cmd/vista/
source.rs

1//! `CmdTableShell` — owns the typed `Table<Cmd, EmptyEntity>` and exposes
2//! it across the `TableShell` boundary.
3//!
4//! `Value = CborValue` end-to-end, so `list_vista_values` forwards the
5//! table's records unchanged. Relations are followed the built-in way:
6//! `get_ref` forwards to the wrapped table's [`Table::get_ref_from_row`],
7//! which reads the join value out of the parent row and pins the target
8//! table with a plain eq-condition — the same path csv / sqlite use.
9
10use async_trait::async_trait;
11use ciborium::Value as CborValue;
12use indexmap::IndexMap;
13use vantage_core::Result;
14use vantage_dataset::traits::ReadableValueSet;
15use vantage_table::table::Table;
16use vantage_types::{EmptyEntity, Record};
17use vantage_vista::{
18    Column as VistaColumn, Reference as VistaReference, TableShell, Vista, VistaCapabilities,
19    VistaMetadata,
20};
21
22use crate::cmd::Cmd;
23use crate::condition::CmdCondition;
24use crate::vista::factory::CmdVistaFactory;
25
26pub struct CmdTableShell {
27    table: Table<Cmd, EmptyEntity>,
28    capabilities: VistaCapabilities,
29    metadata: VistaMetadata,
30}
31
32impl CmdTableShell {
33    pub(crate) fn new(
34        table: Table<Cmd, EmptyEntity>,
35        capabilities: VistaCapabilities,
36        metadata: VistaMetadata,
37    ) -> Self {
38        Self {
39            table,
40            capabilities,
41            metadata,
42        }
43    }
44}
45
46#[async_trait]
47impl TableShell for CmdTableShell {
48    fn columns(&self) -> &IndexMap<String, VistaColumn> {
49        &self.metadata.columns
50    }
51
52    fn references(&self) -> &IndexMap<String, VistaReference> {
53        &self.metadata.references
54    }
55
56    fn id_column(&self) -> Option<&str> {
57        self.metadata.id_column.as_deref()
58    }
59
60    async fn list_vista_values(
61        &self,
62        _vista: &Vista,
63    ) -> Result<IndexMap<String, Record<CborValue>>> {
64        self.table.list_values().await
65    }
66
67    async fn get_vista_value(
68        &self,
69        _vista: &Vista,
70        id: &String,
71    ) -> Result<Option<Record<CborValue>>> {
72        // Route through the typed table so detail-script tables hydrate via
73        // the DETAIL script (Cmd::get_table_value), not the list script.
74        self.table.get_value(id).await
75    }
76
77    async fn get_vista_value_with_row(
78        &self,
79        _vista: &Vista,
80        id: &String,
81        row: &Record<CborValue>,
82    ) -> Result<Option<Record<CborValue>>> {
83        // The detail pass hands us the cheap list-pass row so the detail script
84        // can read columns it carries (e.g. step numbers painted from the
85        // reference conditions).
86        self.table
87            .data_source()
88            .get_table_value_with_row(&self.table, id, row)
89            .await
90    }
91
92    async fn get_vista_some_value(
93        &self,
94        _vista: &Vista,
95    ) -> Result<Option<(String, Record<CborValue>)>> {
96        let data = self.table.list_values().await?;
97        Ok(data.into_iter().next())
98    }
99
100    async fn get_vista_count(&self, _vista: &Vista) -> Result<i64> {
101        Ok(self.table.list_values().await?.len() as i64)
102    }
103
104    fn add_eq_condition(&mut self, field: &str, value: &CborValue) -> Result<()> {
105        self.table
106            .add_condition(CmdCondition::eq(field, value.clone()));
107        Ok(())
108    }
109
110    fn get_ref(&self, relation: &str, row: &Record<CborValue>) -> Result<Vista> {
111        // `Cmd::Value` is `CborValue`, so the row passes through unchanged.
112        // The table's reference machinery reads the join value out of the
113        // parent row and pins the target with a plain eq-condition.
114        let target = self.table.get_ref_from_row::<EmptyEntity>(relation, row)?;
115        CmdVistaFactory::new(self.table.data_source().clone()).from_table(target)
116    }
117
118    fn get_ref_target(&self, relation: &str) -> Result<Vista> {
119        let target = self.table.get_ref_target_erased(relation)?;
120        CmdVistaFactory::new(self.table.data_source().clone()).from_table(target)
121    }
122
123    fn get_ref_kinds(&self) -> Vec<(String, vantage_vista::ReferenceKind)> {
124        self.table.ref_kinds()
125    }
126
127    fn capabilities(&self) -> &VistaCapabilities {
128        &self.capabilities
129    }
130
131    fn driver_name(&self) -> &'static str {
132        "cmd"
133    }
134
135    /// A cmd table's argv is built *by* its Rhai script, and that script builds
136    /// it by calling `run(...)` — so there is no argv to render without running
137    /// the command. What this reports instead is everything that decides the
138    /// argv: the locked program, the script itself, and the values seeded into
139    /// its scope. Read the script against these and you can see the command it
140    /// will assemble.
141    fn preview_query(&self, _vista: &Vista) -> serde_json::Value {
142        let script_name = self.table.table_name();
143        let source = self.table.data_source();
144        let (limit, offset) = match self.table.pagination() {
145            Some(p) => (Some(p.limit()), Some(p.skip())),
146            None => (None, None),
147        };
148        let conditions: Vec<serde_json::Value> = self
149            .table
150            .conditions()
151            .map(|c| {
152                serde_json::json!({
153                    "field": c.field(),
154                    "op": c.op(),
155                    "value": c.json_value(),
156                })
157            })
158            .collect();
159
160        serde_json::json!({
161            "driver": "cmd",
162            "command": source.command(),
163            "script_name": script_name,
164            "script": source
165                .spec_for(script_name)
166                .map(|spec| spec.script.to_string())
167                .unwrap_or_else(|e| format!("<no script registered: {e}>")),
168            "scope": {
169                "columns": self.table.columns().keys().collect::<Vec<_>>(),
170                "conditions": conditions,
171                "limit": limit,
172                "offset": offset,
173            },
174            "note": "the script builds its own argv and runs it; these are the \
175                     scope values it would be evaluated with",
176        })
177    }
178}