fig-schema
The schema layer over fig's value tree: what a
field expects — its type, its allowed values, and how to present it.
fig parses bytes into a fig::Value and edits losslessly; it has no notion of
"what is valid here". This crate adds that knowledge as a generic,
embedder-agnostic engine. It never learns the word "prov" or "flower": a
consumer defines its own constraint type and implements Validate on it, and
FieldRule/Schema are generic over that type — so the path-matching and
commit-time validation plumbing is written once, here, and reused everywhere.
What lives here
| Type | Role |
|---|---|
PathPat / SegPat |
Match a fig path, including every item of a list (EachItem) and whole subtrees (AnyDepth) |
FieldType |
The expected type: coercion of an edit buffer into it (FieldType::coerce), and whether a parsed value already has it (FieldType::admits) |
Term / Cardinality / validate_enum |
A controlled vocabulary and the logic to check a value against one |
Validation / Issue / IssueKind |
Why a value failed, as data rather than prose — or that it was not checked at all (IssueKind::Unchecked) |
Schema::check / Verdict |
A whole document against the schema: every node's shape against its rule's type, and its value against the rule's constraint |
Presentation / Icon / Tint |
Renderer-neutral display hints, carried but never interpreted |
Consequence / Severity |
What changing a field costs, so a host can warn before an expensive or irreversible edit |
lint_vocabulary / Finding |
Judge a vocabulary document rather than read it — what it declares that nothing acts on |
Deliberately not here: a Constraint enum. Whether a field's constraint is a
controlled vocabulary, a reference into a workspace, a range, or a pattern is the
embedder's call.
Example
use Value;
use ;
// The embedder's own constraint type — the seam this crate is built around.
let schema = new;
let path = ;
let rule = schema.rule_for.expect;
assert!;
let rejected = rule.validate;
assert!;
assert_eq!;
// Valid, but not free — ask before committing it.
assert_eq!;
assert_eq!;
The command line
cargo install fig-schema
fig-schema lint vocab/audience.figl
One command so far. lint reads a vocabulary document and reports what it
declares that nothing acts on. Errors are findings that change what
validation does; notes are findings that change only what a reader sees, and
--strict fails on those too. It exits 0 when every file is clean, 1 when a
document has an error, and 2 when the command line itself is wrong — a script
sweeping a directory can tell "fix this document" from "fix this invocation"
without reading the message.
The finding it exists for is values: cloesd. That parses, loads, and validates
— as an open vocabulary, because parse_vocabulary asks only whether the
spelling is exactly closed. Every value the author meant to forbid is then
accepted, and nothing else in this crate can notice, because an open vocabulary
that rejects nothing is indistinguishable from one that was meant to be open.
Installed on PATH it is also fig schema lint <file>: fig hands an action it has
no verb for to a fig-<action> program, passing every argument through
untouched, so the two compose with no registration step anywhere.
The commands with more reach — check, explain, complete — need a Schema,
and a Schema is constructible only in Rust until the schema document format
lands. The format is designed — docs/schema-format.md
— and the loader is the open work; see docs/tasks/.
Two limits worth knowing, both inherited rather than chosen:
- Findings carry a path, not a line and column. fig hands back a value tree
with no per-node spans, so nothing downstream of a parse knows which line a key
came from.
vocabulary.valuesis the most any consumer of the parse can say. - It reads fewer formats than
fig checkdoes. json, jsonc, json5, yaml, toml and figl, plus a markdown file's frontmatter or endmatter block. fig's owncheckalso takes xml, ini, dotenv, properties, nestedtext and the canonical form; the Rust binding has no variant for those, and the formats it does name beyond fig's default language set need the Zig core compiled from source, whichcargo installmust not require.fig convertmoves a document into one of them.
Design notes
Rule precedence is declaration order. Schema::rule_for returns the first
matching rule, so list a specific rule before a broader one that would also match.
Schema::rules_for returns every match in that order — the winner first, then
what it shadows — so a tool can show the precedence rather than leave it inferred.
A check never reports a missing field. A FieldRule says what a value must
be if there is one, and nothing says there must be one, so an empty document is
valid. Nor does it report a node no rule governs: a schema governs what it
names, and a check that fires on correct documents is one people stop running.
Unchecked is not wrong. A constraint of a kind a validator does not know
fails closed — Validation::Reject carrying IssueKind::Unchecked(kind) — so an
editor that does not look further will not commit the value. A reader that does
look can tell unchecked from invalid, and Verdict::is_unchecked is that
look.
Retired terms warn, they don't reject. A Term marked retired is still a
known value — it is merely no longer offered in a picker. A document that
already holds one stays committable, even under a closed vocabulary.
Validation failures are structured. Issue carries the offending value, the
kind of failure, and a near-miss suggestion when one exists. Display renders
a reasonable English default; a frontend that wants to localize the text, or
offer the suggestion as a one-tap correction, has the parts it needs.
A consequence names a destination, not a transition. Consequence::when
matches the value being landed. This crate holds no current value, so it cannot
see a change from anything — which also means asking about a value the field
already holds answers the same as asking about a fresh one. Suppressing that
no-op, and deciding what a deleted field resolves to, are the host's, and the
host is the side that knows.
Costs and tints are different facts. A Tint says how loudly to draw a
field; a Consequence says what happens if the user goes through with the
change. A field can be drawn calmly and still be expensive, or drawn in red and
cost nothing.
Coercion falls back to text. FieldType::coerce never destroys an edit: a
value that doesn't fit the declared type becomes a Value::Str, leaving the
caller's own reparse as the final backstop.
Licence
Licensed under either of Apache License, Version 2.0 or MIT license at your option.