Skip to main content

sim_kernel/catalog/
view.rs

1use std::{
2    collections::{BTreeMap, BTreeSet},
3    sync::Arc,
4};
5
6use crate::{
7    Cx, Error, Expr, Result, Symbol, Value,
8    capability::registry_catalog_read_capability,
9    id::CORE_TABLE_CLASS_ID,
10    object::{ClassRef, Object},
11    table::{Dir, Table},
12};
13
14use super::{CatalogSnapshot, CatalogSnapshotRow};
15
16// sim-non-citizen(reason = "read-only registry catalog view; descriptor data is the catalog snapshot table path", kind = "handle", descriptor = "core/Table")
17/// Read-only directory view over a [`CatalogSnapshot`], navigating table names
18/// as a nested [`Dir`]/[`Table`] tree.
19///
20/// All reads require the `registry.catalog.read` capability; writes fail closed
21/// with [`Error::CatalogReadOnly`]. See the README section "Read-only table
22/// views".
23#[derive(Clone, Debug)]
24pub struct CatalogDirView {
25    snapshot: CatalogSnapshot,
26    path: Vec<Symbol>,
27}
28
29impl CatalogDirView {
30    /// Creates a view rooted at the top of `snapshot`.
31    pub fn new(snapshot: CatalogSnapshot) -> Self {
32        Self {
33            snapshot,
34            path: Vec::new(),
35        }
36    }
37
38    fn at_path(snapshot: CatalogSnapshot, path: Vec<Symbol>) -> Self {
39        Self { snapshot, path }
40    }
41
42    /// Returns the snapshot this view reads from.
43    pub fn snapshot(&self) -> &CatalogSnapshot {
44        &self.snapshot
45    }
46
47    fn table_for_path(&self, path: &[Symbol]) -> Option<Symbol> {
48        table_symbol_for_path(path).filter(|table| self.snapshot.tables.contains_key(table))
49    }
50
51    fn has_namespace_for_path(&self, path: &[Symbol]) -> bool {
52        self.snapshot
53            .tables
54            .keys()
55            .map(table_path)
56            .any(|table_path| path_is_prefix(path, &table_path) && path.len() < table_path.len())
57    }
58
59    fn child_path(&self, name: &Symbol) -> Vec<Symbol> {
60        self.path.iter().cloned().chain(symbol_path(name)).collect()
61    }
62
63    fn child_names(&self) -> Vec<Symbol> {
64        let names = self
65            .snapshot
66            .tables
67            .keys()
68            .map(table_path)
69            .filter_map(|table_path| {
70                path_is_prefix(&self.path, &table_path)
71                    .then(|| table_path.get(self.path.len()).cloned())
72                    .flatten()
73            })
74            .collect::<BTreeSet<_>>();
75        names.into_iter().collect()
76    }
77
78    fn child_value(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>> {
79        let path = self.child_path(&name);
80        if let Some(table) = self.table_for_path(&path) {
81            return cx
82                .factory()
83                .opaque(Arc::new(CatalogTableView {
84                    snapshot: self.snapshot.clone(),
85                    table,
86                }))
87                .map(Some);
88        }
89        if self.has_namespace_for_path(&path) {
90            return cx
91                .factory()
92                .opaque(Arc::new(Self::at_path(self.snapshot.clone(), path)))
93                .map(Some);
94        }
95        Ok(None)
96    }
97
98    fn read_only_error(&self) -> Error {
99        Error::CatalogReadOnly {
100            table: Symbol::new("registry-catalog-view"),
101        }
102    }
103}
104
105impl Object for CatalogDirView {
106    fn display(&self, _cx: &mut Cx) -> Result<String> {
107        Ok(format!("catalog-dir[{}]", path_display(&self.path)))
108    }
109
110    fn as_any(&self) -> &dyn std::any::Any {
111        self
112    }
113}
114
115impl crate::ObjectCompat for CatalogDirView {
116    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
117        let symbol = Symbol::qualified("core", "Table");
118        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
119            return Ok(value.clone());
120        }
121        cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
122    }
123
124    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
125        self.as_table_expr(cx)
126    }
127
128    fn truth(&self, cx: &mut Cx) -> Result<bool> {
129        Ok(!self.is_empty(cx)?)
130    }
131
132    fn as_table_impl(&self) -> Option<&dyn Table> {
133        Some(self)
134    }
135
136    fn as_dir(&self) -> Option<&dyn Dir> {
137        Some(self)
138    }
139}
140
141impl Table for CatalogDirView {
142    fn backend_symbol(&self) -> Symbol {
143        Symbol::qualified("catalog", "dir-view")
144    }
145
146    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
147        cx.require(&registry_catalog_read_capability())?;
148        match self.child_value(cx, key)? {
149            Some(value) => Ok(value),
150            None => cx.factory().nil(),
151        }
152    }
153
154    fn set(&self, _cx: &mut Cx, _key: Symbol, _value: Value) -> Result<()> {
155        Err(self.read_only_error())
156    }
157
158    fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool> {
159        cx.require(&registry_catalog_read_capability())?;
160        let path = self.child_path(&key);
161        Ok(self.table_for_path(&path).is_some() || self.has_namespace_for_path(&path))
162    }
163
164    fn del(&self, _cx: &mut Cx, _key: Symbol) -> Result<Value> {
165        Err(self.read_only_error())
166    }
167
168    fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
169        cx.require(&registry_catalog_read_capability())?;
170        Ok(self.child_names())
171    }
172
173    fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
174        cx.require(&registry_catalog_read_capability())?;
175        self.child_names()
176            .into_iter()
177            .filter_map(|key| {
178                self.child_value(cx, key.clone())
179                    .transpose()
180                    .map(|value| value.map(|value| (key, value)))
181            })
182            .collect()
183    }
184
185    fn len(&self, cx: &mut Cx) -> Result<usize> {
186        cx.require(&registry_catalog_read_capability())?;
187        Ok(self.child_names().len())
188    }
189
190    fn clear(&self, _cx: &mut Cx) -> Result<()> {
191        Err(self.read_only_error())
192    }
193}
194
195impl Dir for CatalogDirView {
196    fn mkdir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Value> {
197        Err(self.read_only_error())
198    }
199
200    fn opendir(&self, cx: &mut Cx, name: Symbol) -> Result<Option<Value>> {
201        cx.require(&registry_catalog_read_capability())?;
202        self.child_value(cx, name)
203    }
204
205    fn rmdir(&self, _cx: &mut Cx, _name: Symbol) -> Result<Value> {
206        Err(self.read_only_error())
207    }
208
209    fn is_dir(&self, cx: &mut Cx, name: Symbol) -> Result<bool> {
210        cx.require(&registry_catalog_read_capability())?;
211        self.has(cx, name)
212    }
213}
214
215// sim-non-citizen(reason = "read-only registry catalog table view; descriptor data is the catalog snapshot table symbol", kind = "handle", descriptor = "core/Table")
216/// Read-only [`Table`] view over the rows of one catalog table in a snapshot.
217///
218/// Reads require `registry.catalog.read`; missing `get` returns `nil`, keys and
219/// entries are sorted by [`Symbol`], and writes fail closed with
220/// [`Error::CatalogReadOnly`].
221#[derive(Clone, Debug)]
222pub struct CatalogTableView {
223    snapshot: CatalogSnapshot,
224    table: Symbol,
225}
226
227impl CatalogTableView {
228    /// Creates a view over `table` within `snapshot`.
229    pub fn new(snapshot: CatalogSnapshot, table: Symbol) -> Self {
230        Self { snapshot, table }
231    }
232
233    /// Returns the table name this view exposes.
234    pub fn table(&self) -> &Symbol {
235        &self.table
236    }
237
238    fn rows(&self) -> impl Iterator<Item = (&Symbol, &CatalogSnapshotRow)> {
239        self.snapshot
240            .rows(&self.table)
241            .into_iter()
242            .flat_map(BTreeMap::iter)
243    }
244}
245
246impl Object for CatalogTableView {
247    fn display(&self, _cx: &mut Cx) -> Result<String> {
248        Ok(format!("catalog-table[{}]", self.table))
249    }
250
251    fn as_any(&self) -> &dyn std::any::Any {
252        self
253    }
254}
255
256impl crate::ObjectCompat for CatalogTableView {
257    fn class(&self, cx: &mut Cx) -> Result<ClassRef> {
258        let symbol = Symbol::qualified("core", "Table");
259        if let Some(value) = cx.registry().class_by_symbol(&symbol) {
260            return Ok(value.clone());
261        }
262        cx.factory().class_stub(CORE_TABLE_CLASS_ID, symbol)
263    }
264
265    fn as_expr(&self, cx: &mut Cx) -> Result<Expr> {
266        self.as_table_expr(cx)
267    }
268
269    fn truth(&self, cx: &mut Cx) -> Result<bool> {
270        Ok(!self.is_empty(cx)?)
271    }
272
273    fn as_table_impl(&self) -> Option<&dyn Table> {
274        Some(self)
275    }
276}
277
278impl Table for CatalogTableView {
279    fn backend_symbol(&self) -> Symbol {
280        Symbol::qualified("catalog", "table-view")
281    }
282
283    fn get(&self, cx: &mut Cx, key: Symbol) -> Result<Value> {
284        cx.require(&registry_catalog_read_capability())?;
285        match self
286            .snapshot
287            .rows(&self.table)
288            .and_then(|rows| rows.get(&key))
289        {
290            Some(row) => row_value(cx, row),
291            None => cx.factory().nil(),
292        }
293    }
294
295    fn set(&self, _cx: &mut Cx, _key: Symbol, _value: Value) -> Result<()> {
296        Err(Error::CatalogReadOnly {
297            table: self.table.clone(),
298        })
299    }
300
301    fn has(&self, cx: &mut Cx, key: Symbol) -> Result<bool> {
302        cx.require(&registry_catalog_read_capability())?;
303        Ok(self
304            .snapshot
305            .rows(&self.table)
306            .is_some_and(|rows| rows.contains_key(&key)))
307    }
308
309    fn del(&self, _cx: &mut Cx, _key: Symbol) -> Result<Value> {
310        Err(Error::CatalogReadOnly {
311            table: self.table.clone(),
312        })
313    }
314
315    fn keys(&self, cx: &mut Cx) -> Result<Vec<Symbol>> {
316        cx.require(&registry_catalog_read_capability())?;
317        Ok(self.rows().map(|(key, _)| key.clone()).collect())
318    }
319
320    fn entries(&self, cx: &mut Cx) -> Result<Vec<(Symbol, Value)>> {
321        cx.require(&registry_catalog_read_capability())?;
322        self.rows()
323            .map(|(key, row)| row_value(cx, row).map(|value| (key.clone(), value)))
324            .collect()
325    }
326
327    fn len(&self, cx: &mut Cx) -> Result<usize> {
328        cx.require(&registry_catalog_read_capability())?;
329        Ok(self.rows().count())
330    }
331
332    fn clear(&self, _cx: &mut Cx) -> Result<()> {
333        Err(Error::CatalogReadOnly {
334            table: self.table.clone(),
335        })
336    }
337}
338
339/// Returns a [`CatalogDirView`] over the registry's catalog snapshot, gated by
340/// the `registry.catalog.read` capability.
341pub fn registry_catalog_view(cx: &mut Cx) -> Result<Value> {
342    cx.require(&registry_catalog_read_capability())?;
343    let snapshot = cx.registry().catalog_snapshot();
344    cx.factory().opaque(Arc::new(CatalogDirView::new(snapshot)))
345}
346
347fn row_value(cx: &mut Cx, row: &CatalogSnapshotRow) -> Result<Value> {
348    let entries = row
349        .data
350        .iter()
351        .map(|(field, value)| {
352            cx.factory()
353                .expr(value.clone())
354                .map(|value| (field.clone(), value))
355        })
356        .collect::<Result<Vec<_>>>()?;
357    cx.factory().table(entries)
358}
359
360fn symbol_path(symbol: &Symbol) -> impl Iterator<Item = Symbol> {
361    symbol
362        .as_qualified_str()
363        .split('/')
364        .map(|part| Symbol::new(part.to_owned()))
365        .collect::<Vec<_>>()
366        .into_iter()
367}
368
369fn table_path(table: &Symbol) -> Vec<Symbol> {
370    symbol_path(table).collect()
371}
372
373fn table_symbol_for_path(path: &[Symbol]) -> Option<Symbol> {
374    match path {
375        [name] => Some(Symbol::new(name.as_qualified_str())),
376        [namespace, name] => Some(Symbol::qualified(
377            namespace.as_qualified_str(),
378            name.as_qualified_str(),
379        )),
380        _ => None,
381    }
382}
383
384fn path_is_prefix(prefix: &[Symbol], path: &[Symbol]) -> bool {
385    prefix.len() <= path.len() && prefix.iter().zip(path).all(|(left, right)| left == right)
386}
387
388fn path_display(path: &[Symbol]) -> String {
389    if path.is_empty() {
390        "/".to_owned()
391    } else {
392        path.iter()
393            .map(Symbol::as_qualified_str)
394            .collect::<Vec<_>>()
395            .join("/")
396    }
397}