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