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` forwards the table's records
5//! unchanged. A clone copies the table's conditions and shares the `Cmd`.
6//! Equality conditions reach the script as its `conditions` scope and are
7//! re-applied to rows that carry the field. Windows are cut from the listed
8//! rows in memory, since a script may ignore the `limit`/`offset` it is
9//! handed. Joins narrow the target through `Reference::narrow`.
10
11use async_trait::async_trait;
12use ciborium::Value as CborValue;
13use indexmap::IndexMap;
14use vantage_core::Result;
15use vantage_dataset::traits::ReadableValueSet;
16use vantage_table::table::Table;
17use vantage_types::{EmptyEntity, Record};
18use vantage_vista::{
19    Capability, Condition, FilterOp, Page, Preview, TableShell, Transaction, Vista,
20    VistaCapabilities, VistaMetadata, condition_column_error, direct, unknown_relation,
21    unsupported,
22};
23
24use crate::cmd::Cmd;
25use crate::condition::CmdCondition;
26use crate::vista::factory::CmdVistaFactory;
27
28#[derive(Clone)]
29pub struct CmdTableShell {
30    table: Table<Cmd, EmptyEntity>,
31    capabilities: VistaCapabilities,
32    metadata: VistaMetadata,
33}
34
35impl CmdTableShell {
36    pub(crate) fn new(
37        table: Table<Cmd, EmptyEntity>,
38        capabilities: VistaCapabilities,
39        metadata: VistaMetadata,
40    ) -> Self {
41        Self {
42            table,
43            capabilities,
44            metadata,
45        }
46    }
47
48    /// Everything that decides the argv: the locked program, the script, and
49    /// the values seeded into its scope.
50    fn preview_scope(&self) -> serde_json::Value {
51        let script_name = self.table.table_name();
52        let source = self.table.data_source();
53        let conditions: Vec<serde_json::Value> = self
54            .table
55            .conditions()
56            .map(|c| {
57                serde_json::json!({
58                    "field": c.field(),
59                    "op": c.op(),
60                    "value": c.json_value(),
61                })
62            })
63            .collect();
64        serde_json::json!({
65            "command": source.command(),
66            "script_name": script_name,
67            "script": source
68                .spec_for(script_name)
69                .map(|spec| spec.script.to_string())
70                .unwrap_or_else(|e| format!("<no script registered: {e}>")),
71            "scope": {
72                "columns": self.table.columns().keys().collect::<Vec<_>>(),
73                "conditions": conditions,
74            },
75        })
76    }
77}
78
79#[async_trait]
80impl TableShell for CmdTableShell {
81    fn metadata(&self) -> &VistaMetadata {
82        &self.metadata
83    }
84
85    fn capabilities(&self) -> &VistaCapabilities {
86        &self.capabilities
87    }
88
89    fn driver_name(&self) -> &'static str {
90        "cmd"
91    }
92
93    /// A cmd table's argv is built *by* its Rhai script, which calls `run(...)`
94    /// itself, so there is no argv to render without running the command. The
95    /// query is what decides it instead: the command, the script, and the
96    /// scope it is evaluated with, every condition included.
97    fn preview(&self) -> Preview {
98        Preview::new().with_query(self.preview_scope().to_string())
99    }
100
101    async fn list(&self) -> Result<IndexMap<String, Record<CborValue>>> {
102        self.table.list_values().await
103    }
104
105    /// The command's DETAIL script when the table declares one (a direct
106    /// lookup by id); otherwise the set is listed and the row picked out.
107    /// The detail script doesn't see the conditions, so on a narrowed set
108    /// the id must first be listed in the set.
109    async fn get(&self, id: &str) -> Result<Option<Record<CborValue>>> {
110        let has_detail = self
111            .table
112            .data_source()
113            .has_detail_script(self.table.table_name());
114        if self.table.conditions().next().is_none() || !has_detail {
115            return self.table.get_value(id).await;
116        }
117        if !self.table.list_values().await?.contains_key(id) {
118            return Ok(None);
119        }
120        self.table.get_value(id).await
121    }
122
123    async fn count(&self) -> Result<i64> {
124        Ok(self.table.list_values().await?.len() as i64)
125    }
126
127    /// The window is cut from the listed rows, which are all read anyway, so
128    /// the page carries the set's total for free.
129    async fn fetch_window(&self, offset: usize, limit: usize) -> Result<Page> {
130        Ok(Page::from_listing(
131            self.table.list_values().await?,
132            offset,
133            limit,
134        ))
135    }
136
137    /// Equality on a declared column. The script receives it in its
138    /// `conditions` scope; no other operator is passed on. A dotted path
139    /// into a declared column is refused, for the caller to evaluate.
140    fn add_condition(&mut self, condition: &Condition) -> Result<()> {
141        let field = condition.column.as_str();
142        if !self.metadata.columns.contains_key(field) {
143            return Err(condition_column_error(&self.metadata, field));
144        }
145        if condition.op != FilterOp::Eq {
146            return unsupported(Capability::FilterOps);
147        }
148        self.table
149            .add_condition(CmdCondition::eq(field, condition.value.clone()));
150        Ok(())
151    }
152
153    /// The target comes from the table's `with_one`/`with_many`; from a row
154    /// it is narrowed by the reference's join keys.
155    fn get_ref(&self, relation: &str, row: Option<&Record<CborValue>>) -> Result<Vista> {
156        let reference = self
157            .metadata
158            .references
159            .get(relation)
160            .ok_or_else(|| unknown_relation(relation))?;
161        self.table.traverse_erased(reference, row, |target| {
162            CmdVistaFactory::new(self.table.data_source().clone()).from_table(target)
163        })
164    }
165
166    /// A command has no transactions: inside one, the shell works as it
167    /// does outside.
168    fn rebind(&self, _tx: &Transaction) -> Result<Box<dyn TableShell>> {
169        Ok(direct(Box::new(self.clone())))
170    }
171}