Skip to main content

vantage_vista/
capabilities.rs

1//! What a shell can do: [`Capability`], the [`VistaCapabilities`] set a shell
2//! advertises, and [`unsupported`], the refusal a shell answers with.
3
4use std::fmt;
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use vantage_core::{Result, VantageError, error};
8
9mod set;
10
11pub use set::VistaCapabilities;
12
13/// One method family of [`TableShell`](crate::TableShell) that a shell may or
14/// may not implement.
15///
16/// Each capability names exactly one family, and a shell advertising it must
17/// answer that family without refusing (the `contract` feature checks this):
18///
19/// | Capability | Methods |
20/// |---|---|
21/// | `Insert` | `insert` |
22/// | `Update` | `replace`, `upsert`, `patch` |
23/// | `Delete` | `delete`, `delete_all` |
24/// | `DeleteAll` | `delete_all`; implied by `Delete`, so a shell doesn't advertise it. It exists so a rule can deny bulk delete alone |
25/// | `Import` | `import`: one atomic batch that counts only new rows |
26/// | `Subscribe` | `watch` |
27/// | `Order` | `set_order` |
28/// | `Search` | `set_search` |
29/// | `FilterOps` | `add_condition` with any operator other than `Eq` |
30/// | `NativeConditions` | `add_native_condition` |
31/// | `Window` | `fetch_window` at any offset |
32/// | `Cursor` | `fetch_cursor` |
33/// | `Traverse` | `get_ref` |
34/// | `ConfineWrites` | every write keeps to the narrowed set (see `WritableValueSet`) |
35/// | `WritesReachHooks` | every write runs through a `Table`, so its hooks (audit, generated ids, access checks) see it |
36///
37/// `list`, `get`, `count` and equality conditions have no capability: every
38/// shell answers them. Nor does the leading window, `fetch_window(0, n)`,
39/// which every shell that lists answers.
40#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
41#[non_exhaustive]
42pub enum Capability {
43    Insert,
44    Update,
45    Delete,
46    DeleteAll,
47    Import,
48    Subscribe,
49    Order,
50    Search,
51    FilterOps,
52    NativeConditions,
53    Window,
54    Cursor,
55    Traverse,
56    ConfineWrites,
57    WritesReachHooks,
58}
59
60/// Every variant, in declaration order. The set is a `u16` bitset, so this
61/// holds at most 16.
62const ALL: [Capability; 15] = [
63    Capability::Insert,
64    Capability::Update,
65    Capability::Delete,
66    Capability::DeleteAll,
67    Capability::Import,
68    Capability::Subscribe,
69    Capability::Order,
70    Capability::Search,
71    Capability::FilterOps,
72    Capability::NativeConditions,
73    Capability::Window,
74    Capability::Cursor,
75    Capability::Traverse,
76    Capability::ConfineWrites,
77    Capability::WritesReachHooks,
78];
79
80impl Capability {
81    /// Every capability, in declaration order. Serde, the rhai
82    /// `capabilities()` map and the contract checks all iterate this.
83    pub fn iter() -> impl Iterator<Item = Capability> {
84        ALL.into_iter()
85    }
86
87    /// The snake_case name used in serde, scripts and error context.
88    pub fn as_str(self) -> &'static str {
89        match self {
90            Capability::Insert => "insert",
91            Capability::Update => "update",
92            Capability::Delete => "delete",
93            Capability::DeleteAll => "delete_all",
94            Capability::Import => "import",
95            Capability::Subscribe => "subscribe",
96            Capability::Order => "order",
97            Capability::Search => "search",
98            Capability::FilterOps => "filter_ops",
99            Capability::NativeConditions => "native_conditions",
100            Capability::Window => "window",
101            Capability::Cursor => "cursor",
102            Capability::Traverse => "traverse",
103            Capability::ConfineWrites => "confine_writes",
104            Capability::WritesReachHooks => "writes_reach_hooks",
105        }
106    }
107
108    /// The capability named `name` (its [`as_str`](Self::as_str) form).
109    pub(crate) fn parse(name: &str) -> Option<Capability> {
110        Capability::iter().find(|c| c.as_str() == name)
111    }
112
113    fn bit(self) -> u16 {
114        1 << (self as u16)
115    }
116}
117
118impl fmt::Display for Capability {
119    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
120        f.write_str(self.as_str())
121    }
122}
123
124impl Serialize for Capability {
125    fn serialize<S: Serializer>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error> {
126        serializer.serialize_str(self.as_str())
127    }
128}
129
130impl<'de> Deserialize<'de> for Capability {
131    fn deserialize<D: Deserializer<'de>>(deserializer: D) -> std::result::Result<Self, D::Error> {
132        let name = String::deserialize(deserializer)?;
133        Capability::parse(&name)
134            .ok_or_else(|| serde::de::Error::custom(format!("unknown capability `{name}`")))
135    }
136}
137
138/// The refusal of a method whose capability the shell doesn't have: an
139/// `Unsupported` error naming the capability. Default trait methods answer
140/// with it; a caller probing a capability reads it as "no", not as a fault.
141pub fn unsupported<T>(capability: Capability) -> Result<T> {
142    Err(error!(
143        "capability is not supported; method refused",
144        capability = capability.as_str()
145    )
146    .mark_unsupported()
147    .traced_debug())
148}
149
150/// The error for a relation a shell doesn't declare, marked
151/// [`not_found`](vantage_core::VantageError::is_not_found) so a caller can
152/// tell a missing relation from one that fails to traverse.
153pub fn unknown_relation(relation: &str) -> VantageError {
154    error!("unknown relation", relation = relation).mark_not_found()
155}