fig_schema/lib.rs
1//! `fig-schema` — the schema layer: what a field *expects* — its type, its
2//! allowed values, and how to present it — layered over [`fig`]'s schema-free
3//! value tree.
4//!
5//! fig parses bytes → [`fig::Value`] and edits losslessly; it has no notion of
6//! "what is valid here". This crate adds that knowledge as a **generic,
7//! embedder-agnostic** engine. It never learns the word "prov" or "flower": a
8//! consumer (prov, for frontmatter fields; flower, for a metadata editor)
9//! defines its own constraint type — an enum covering whatever kinds of
10//! constraint it needs (a controlled vocabulary, a reference into a
11//! workspace, …) — and implements [`Validate`] on it. [`FieldRule`]/[`Schema`]
12//! are generic over that type, so the path-matching and commit-time
13//! validation plumbing is written once, here, and reused by every embedder.
14//!
15//! What's genuinely reusable, and lives here as concrete types rather than
16//! being left to the embedder:
17//!
18//! - [`PathPat`] / [`SegPat`] — pattern-matching a fig path, including "every
19//! item of this list" ([`SegPat::EachItem`]) and "this subtree"
20//! ([`SegPat::AnyDepth`]).
21//! - [`FieldType`] — the expected type, and type-directed coercion of an edit
22//! buffer ([`FieldType::coerce`]).
23//! - [`Term`] / [`Cardinality`] / [`validate_enum`] — a controlled vocabulary
24//! and the logic to check a value against one (closed-vocabulary rejection,
25//! open-vocabulary near-miss warnings). Cardinality (one vs. many) is pure
26//! data shape, useful even to a constraint this crate doesn't otherwise model
27//! (a relation/reference field, for instance).
28//! - [`VocabularyDoc`] / [`parse_vocabulary`] — load a term set from the
29//! shared `vocabulary: { field, values }` / `terms:` document convention, so
30//! independent embedders can point at the same vocabulary document without
31//! either depending on the other.
32//! - [`Presentation`] / [`Icon`] / [`Tint`] — renderer-neutral display hints,
33//! carried on every rule but never interpreted here.
34//! - [`Consequence`] / [`Severity`] — what changing a field *costs*, so a host
35//! can warn before committing an expensive or irreversible edit. A separate
36//! fact from a [`Tint`], which says only how loudly to draw the field.
37//!
38//! - [`Issue`] / [`IssueKind`] — why a value failed, as data rather than
39//! prose, so the embedder owns the wording. [`Issue`]'s `Display` renders a
40//! reasonable English default for embedders that don't care.
41//!
42//! The public structs are `#[non_exhaustive]`, so they are built from a
43//! constructor plus chainable setters ([`FieldRule::new`], [`Term::value`],
44//! [`Presentation::default`]) rather than a struct literal. That is what lets a
45//! later release add a hint without costing every embedder a major version.
46//!
47//! # Example
48//!
49//! ```
50//! use fig::Value;
51//! use fig_schema::{
52//! Consequence, FieldRule, FieldType, PathPat, Presentation, Schema, Seg, Severity,
53//! Term, Validate, Validation, validate_enum,
54//! };
55//!
56//! // The embedder's own constraint type — the seam this crate is built around.
57//! struct Vocabulary { values: Vec<Term>, closed: bool }
58//!
59//! impl Validate for Vocabulary {
60//! fn validate(&self, value: &Value) -> Validation {
61//! validate_enum(&self.values, self.closed, value)
62//! }
63//! }
64//!
65//! let schema = Schema::new(vec![
66//! FieldRule::new(PathPat::each_item_of("audience"))
67//! .ty(FieldType::Str)
68//! .constraint(Vocabulary {
69//! values: vec![Term::value("public"), Term::value("family")],
70//! closed: true,
71//! })
72//! .present(Presentation::default().title("Audience"))
73//! .on_change(
74//! Consequence::when("public", "Anyone with the link will be able to read this.")
75//! .severity(Severity::Confirm),
76//! ),
77//! ]);
78//!
79//! // Find the rule governing `audience[0]`, then check a candidate against it.
80//! let path = [Seg::Key("audience".into()), Seg::Index(0)];
81//! let rule = schema.rule_for(&path).expect("a rule governs this path");
82//! assert!(rule.validate(&Value::Str("public".into())).is_ok());
83//!
84//! // Valid, but not free — ask before committing it.
85//! assert_eq!(
86//! rule.severity_of(&Value::Str("public".into())),
87//! Some(Severity::Confirm),
88//! );
89//! assert_eq!(rule.severity_of(&Value::Str("family".into())), None);
90//!
91//! let rejected = rule.validate(&Value::Str("familly".into()));
92//! assert!(rejected.is_reject());
93//! assert_eq!(
94//! rejected.issue().unwrap().suggestion.as_deref(),
95//! Some("family"),
96//! );
97//! ```
98
99mod consequence;
100mod field;
101mod path;
102mod present;
103mod vocab;
104
105pub use consequence::{Consequence, Severity, guards_without_terms};
106pub use field::{FieldRule, FieldType, Schema};
107pub use path::{PathPat, Seg, SegPat};
108pub use present::{Icon, Presentation, Tint};
109pub use vocab::{
110 Cardinality, Issue, IssueKind, Term, Validate, Validation, VocabularyDoc, parse_vocabulary,
111 validate_enum,
112};