vantage-vista 2.1.0

Universal, schema-bearing data handle for the Vantage data framework
Documentation

Vantage Vista

Vista is the type-erased Table<T, E>. It takes a typed table, the kind a model layer defines (bakery_model3::Product::sqlite_table(), vantage_aws::models::ecs::clusters_table()), and seals its types behind a TableShell, so generic code (a CLI, a desktop UI, a config-driven dashboard) can drive any backend through one handle. Rows travel as CBOR (ciborium::Value) and ids as String, whatever the backend stores natively.

The shape is the same as Table's: Vista is Clone, owns its query state, narrows with with_* and changes in place with add_*/set_*.

Three kinds of object

Every public type in Vantage above Table is one of three kinds, and its API follows from the kind. A value (Table, Vista, VistaQuery, Reference) is narrowed by clone().with_*(), which returns a new value and leaves the original alone, and changes through add_*/set_* only when you own it (&mut self). A shared handle (diorama's Lens and Dio) never changes after construction: with_* derives a new handle. View state (diorama's sceneries and Servo) is opened by a builder and changed with set_* by its single owner. This crate holds only values; the other two kinds live in vantage-diorama.

Examples

Build a Vista

A driver factory turns a typed table into a Vista:

use vantage_sql::sqlite::vista::SqliteVistaFactory;

let products = SqliteVistaFactory::new(db.clone()).from_table(Product::sqlite_table(db))?;

from_table consumes the typed table and gives a Vista that knows its columns, references, id column and the backend's capabilities. The schema is fixed from there; Vista has no add_column.

With no Rust entity for the table, the same factory builds one from a YAML spec:

use vantage_vista::VistaFactory;

let products = factory.from_yaml(include_str!("products.yaml"))?;

A driver author wraps a shell directly:

use vantage_vista::{TableShell, Vista};

let vista = Vista::new("product", MyShell::new(conn));

VistaFactory and TableShell are imported from the crate root, not the prelude: only driver code and spec loaders name them.

Narrow

use vantage_vista::prelude::*;

let pastries = products
    .clone()
    .with_condition("category", FilterOp::Eq, "pastries")?
    .with_order("price", SortDirection::Descending)?;

let mut cheap = products.clone();
cheap.add_condition("price", FilterOp::Lt, 5)?;
cheap.set_search(Some("tart"))?;

products is untouched by both. Each clause is offered to the shell first and recorded only once the shell accepts it, so pastries.query() is a VistaQuery holding exactly what was applied. A clause the backend can't take is refused with an Unsupported error naming the Capability it lacks. An unknown column is a plain error, so a typo is never mistaken for a missing feature.

A query as a value

VistaQuery is plain data: Conditions (column <op> value), a search text, an order ((column, SortDirection)) and the keys of any driver-native conditions. Build one and apply it:

let query = VistaQuery::new()
    .with_condition(Condition::new("category", FilterOp::Eq, "pastries"))
    .with_order("price", SortDirection::Ascending);

let (pastries, residual) = products.clone().with_query(&query)?;

residual holds the clauses the shell refused. A caller that needs every clause applied checks it is empty; a cache or a client-side refine evaluates it over the rows itself, with residual.matches(&row, &metadata) and residual.sort_rows(&mut rows, &metadata) (the evaluator documented in vantage_vista::eval). query.key() is a stable string for cache identity.

Read and write

Vista implements ValueSet, ReadableValueSet, WritableValueSet and InsertableValueSet, the same trait family Table<T, E> does:

let rows = pastries.list_values().await?;
let total = pastries.count().await?;
let page = pastries.fetch_window(40, 20).await?;   // page.rows, page.total

The leading window (fetch_window(0, n)) works on every shell. Any other offset needs Capability::Window; a forward-only backend offers fetch_cursor under Capability::Cursor. A write through a narrowed Vista needs Capability::ConfineWrites, so it can't land outside the set.

Capabilities

let caps = products.capabilities();
if caps.has(Capability::Insert) { /* show "+ New" */ }
if caps.has(Capability::Order) { /* render sortable headers */ }
if caps.has(Capability::Search) { /* render the quicksearch box */ }
if caps.has(Capability::Subscribe) { /* follow products.watch() */ }

Each Capability names exactly one method family, and a shell that advertises one must answer it without refusing; the contract feature checks that. A read-only AWS API and a writable SQLite table look the same from outside; the set tells the UI which controls to draw.

Catalog and resolvers

VistaResolver is the one way to turn a name into a Vista: a model in a catalog, a relation's target, a table a script names. Any Fn(&str) -> Result<Vista> is one. Catalog maps model names to loaders, which may sit on different backends, and is itself a resolver:

use vantage_vista::Catalog;

let catalog = Catalog::new()
    .with("client", |name: &str| clients_vista(name))
    .with("order", |name: &str| orders_vista(name));

let orders = catalog.vista("order")?;

A name the resolver doesn't know is not_found; a loader that fails for another reason keeps its own error kind, so a caller can tell a missing model from an unreachable one. Within one backend, vista.get_ref("orders", Some(&row)) traverses a declared reference; across backends, narrow the catalog's target with reference.narrow(catalog.vista(&reference.target)?, &row).

Scripting with Rhai

The rhai feature adds the rhai module and its DataVocab, the one data vocabulary every Vantage script uses:

let due = table("order").where("status", "due");
due.ref("client").list()

table(name) returns a handle that narrows (where, sort, search, limit, ref) and then reads (list, get, first, count, ids), writes (insert, upsert, patch, delete, import_from) or opens a record draft. A host picks the tables behind table(name) and which verbs exist. The full guide is Scripting with Rhai in the Vantage book.

Why not a stateless shell?

A shell that held no query, with every read taking a VistaQuery, would make Vista a thin wrapper and drivers simpler. It was considered and rejected: Vista would stop matching Table<T, E>, which owns its conditions, and code written against the dataset traits would need a query threaded through every call. The cost of the path taken is that a driver's shell is Clone and carries its narrowing, and a clone must not share it.

Prelude

use vantage_vista::prelude::*; brings in Vista, VistaQuery, Condition, FilterOp, SortDirection, Capability, VistaResolver, CborValue, and the vantage-dataset traits that Vista implements (ValueSet, ReadableValueSet, WritableValueSet, InsertableValueSet).

Everything else is reached from the crate root: VistaFactory and TableShell for driver authors, Catalog, Column, Reference, Page, VistaMetadata. VistaCapabilities is what vista.capabilities() returns.

Features

  • mocks: vantage_vista::mocks::MockShell, an in-memory TableShell for tests and examples. Enable it from [dev-dependencies].
  • rhai: the rhai module (DataVocab, the table handle, script-slot evaluators, run_script). Backends add their vendor expression vocabulary through TableShell::register_rhai_extensions.
  • contract: vantage_vista::contract, the driver-facing checks every backend's test suite runs against its own fixture. Enable it from [dev-dependencies].

A test Vista over MockShell:

use vantage_vista::mocks::MockShell;

let users = Vista::new("user", MockShell::new().with_record("u1", record));

Integration

  • vantage-dataset: the ValueSet traits Vista implements
  • vantage-types: the Record<V> carrier
  • vantage-core: Result / VantageError
  • vantage-rhai: the script host behind the rhai feature
  • vantage-diorama: the Lens/Dio cache and the sceneries that sit on a Vista