Schemas
How a scope's struct describes itself, and how its declarations become its starting value.
Every scope is a Rust struct that implements Schema. The schema does two jobs. It describes the struct's shape as data, so the store can claim and name fields and so a validator can check what views read. And it builds the struct's starting value from the same declarations, so the two can never disagree.
No derive yet. A
#[derive]forSchemaexists, but it lives in a separate, unpublished crate. Until it moves into Carte you write the impl by hand, as on this page. TODO: the derive's name and attribute syntax once it ships.
The trait
pub trait Schema {
fn reflect() -> Kind;
fn at_mount(declared: Option<&Lit>) -> Self where Self: Sized;
fn type_name() -> Option<&'static str> where Self: Sized { None }
}
reflectreturns the type's shape.at_mountreturns the value a field of this type holds when its scope mounts.declaredis the starting value the schema declared, orNoneif it declared nothing.type_nameis only for recursive types. See below.
A hand-written schema
use carte::schema::{Field, Kind, Lit, Schema};
#[derive(Clone, Debug, PartialEq)]
struct Editor {
title: String,
dirty: bool,
words: usize,
}
impl Schema for Editor {
fn reflect() -> Kind {
Kind::Record(vec![
Field::new("title", Kind::Str).starting_at(Lit::Str("Untitled".into())),
Field::new("dirty", Kind::Bool),
Field::new("words", Kind::Int),
])
}
fn at_mount(_: Option<&Lit>) -> Self {
Self {
title: String::at_mount(Some(&Lit::Str("Untitled".into()))),
dirty: bool::at_mount(None),
words: usize::at_mount(None),
}
}
}
The field names in reflect must match the struct's fields, because producers set fields by name. The set! macro takes the name and the field from one token, so a producer cannot set one under the other's name. See Producers and the tick.
By hand, you also have to keep reflect and at_mount in step. Above, "Untitled" is written twice. This is the duplication the derive removes: it emits both from one attribute on the field.
The declaration is the starting value
Store::provides takes no value. It builds the starting value by calling at_mount(None) on the type, and every mount starts from a copy of it. So "the schema says title starts as Untitled" and "the scope mounted with title Untitled" are one fact, not two that have to agree.
A starting value matters because a silent zero can look exactly like a bug: a fade that starts at 0.0 draws perfectly and shows nothing. So Field records whether anybody chose the value:
Initial::Declared(lit)when the schema said so, withstarting_at(lit).Initial::Implied(lit)when it fell through to the kind's zero: an empty string,0,false, an enum's first name.
A starting value a type cannot hold, such as a string literal for an integer field, panics at mount and names both. It is a startup bug with a declaration behind it.
Kinds
Kind is the shape vocabulary:
| Kind | Rust types with a built-in Schema impl |
|---|---|
Str | String |
Int | i8 to i64, isize, u8 to u64, usize |
Float | f32, f64 |
Bool | bool |
List(kind) | Vec<T> |
Map(kind) | BTreeMap<K, V>, HashMap<K, V> (only the value is part of the shape) |
Tuple(kinds) | Tuples of two to four, and [T; N] |
Record(fields) | Your structs |
Enum(names) | Payload-free enums, written by hand |
Union(variants) | Enums with payloads, written by hand |
Cycle | A back-edge in a recursive type |
Use reflect_of::<T>() to get a kind from a type, such as reflect_of::<Vec<(usize, String)>>(). It is cycle-safe: a type that reaches itself and returns Some from type_name reflects the back-edge as Kind::Cycle.
Field declarations
Field::new(name, kind) makes a required field with an implied starting value and exact precision. Three builders add declarations:
starting_at(lit)declares the starting value.absent_when(sentence)says the field may be absent, and when, as one sentence: "until the list has loaded". Optionality is stated on the field, not implied by a wrapper.at_grain(step)declares the field's precision.set_num!floors values to it. See Producers and the tick.
TODO: there is no
Schemaimpl forOption<T>in the crate today. How an absent field is represented in the Rust struct is not settled.
Structural comparison
Kind::compare checks two shapes structurally. Field names and kinds must match. Type names are never compared, and field order does not matter. Presence, starting values and grain are not compared either, because two providers of the same shape may legitimately differ there. A mismatch is a Mismatch whose field() is the dotted path to where the shapes stop agreeing, and whose difference() says how.
Kind::field_at("a.b") resolves a dotted path through records. It refuses to step into a list, a map, a union or a back-edge: a collection is one field.
The printed form
Kind implements Display, printing a small schema language. It is what an error quotes when it says what shape it wanted. Declared starting values print after =, grains after @:
{
title: str = "Untitled",
dirty: bool,
words: int,
}
It only prints. Nothing parses it back.
Next
Producers and the tick: writing those fields.