Skip to main content

provui_core/
rules.rs

1//! The rule builders [`config_schema`](crate::config_schema) is written in.
2//!
3//! Public, and deliberately so. A [`Schema`](flower_core::Schema) resolves a
4//! path by **first match wins** (`rule_for` returns the first rule whose pattern
5//! matches), so an application that keeps its own keys in a config document —
6//! prov permits a config surface to carry fields it never reads — composes by
7//! *prepending* its rules to [`config_rules`](crate::config_schema::config_rules):
8//!
9//! ```ignore
10//! let mut rules = my_app_rules();              // `myapp.*`, and any narrowing
11//! rules.extend(provui_core::config_schema::config_rules(&config));
12//! let schema = Schema::new(rules);
13//! ```
14//!
15//! Prepending is what makes an *overlay* possible rather than only an addition:
16//! an app that wants a narrower vocabulary for a key this crate governs openly
17//! (`views.*.group`, say) puts its own rule first and shadows the generic one.
18//! Appending would leave the generic rule winning and the app's rule dead.
19//!
20//! These builders exist so an overlay's rows come out looking like the ones
21//! beside them — same tints, same consequence vocabulary — without every
22//! frontend restating what a "costly" field looks like.
23
24use fig::Value;
25use flower_core::schema::{Constraint, FieldRule};
26use flower_core::{
27    Consequence, FieldType, Icon, PathPat, Presentation, SegPat, Severity, Term, Tint,
28};
29
30/// A dotted path pattern: `*` means "any key at this depth" (`fields.*.type`),
31/// `[]` means "each item of this sequence" (`audiences.[].name`).
32///
33/// The two are not interchangeable and the difference is easy to get wrong in
34/// the silent direction: an `*` against a sequence matches nothing, governs
35/// nothing, and reports no error — the rows simply come out untyped.
36pub fn path(segments: &[&str]) -> PathPat {
37    PathPat(
38        segments
39            .iter()
40            .map(|s| match *s {
41                "*" => SegPat::AnyKey,
42                "[]" => SegPat::EachItem,
43                key => SegPat::Key(key.to_string()),
44            })
45            .collect(),
46    )
47}
48
49/// A titled, icon-bearing presentation.
50pub fn present(title: &str, icon: Icon) -> Presentation {
51    Presentation::default().title(title).icon(icon)
52}
53
54/// A vocabulary term with an optional one-line gloss (an empty gloss is none).
55pub fn term(value: &str, gloss: &str) -> Term {
56    Term::value(value).description_opt((!gloss.is_empty()).then_some(gloss))
57}
58
59/// A free-text field.
60pub fn text(at: PathPat, title: &str, icon: Icon) -> FieldRule {
61    FieldRule::new(at)
62        .ty(FieldType::Str)
63        .present(present(title, icon))
64}
65
66/// A boolean field.
67pub fn toggle(at: PathPat, title: &str) -> FieldRule {
68    FieldRule::new(at)
69        .ty(FieldType::Bool)
70        .present(present(title, Icon::Toggle))
71}
72
73/// A closed pick-list: anything else is a value prov would ignore, so the editor
74/// rejects it rather than writing it and letting the default quietly win.
75pub fn choice(at: PathPat, title: &str, icon: Icon, values: &[(&str, &str)]) -> FieldRule {
76    choice_terms(
77        at,
78        title,
79        icon,
80        values.iter().map(|(v, g)| term(v, g)).collect(),
81    )
82}
83
84/// [`choice`] over terms already built.
85pub fn choice_terms(at: PathPat, title: &str, icon: Icon, values: Vec<Term>) -> FieldRule {
86    FieldRule::new(at)
87        .ty(FieldType::Str)
88        .constraint(Constraint::Enum {
89            values,
90            closed: true,
91        })
92        .present(present(title, icon))
93}
94
95/// An offered-but-not-enforced pick-list — for a vocabulary this crate cannot
96/// see the whole of (see `metadata.format`), or one where a value it does not
97/// know is still legitimate (see `views.*.group`).
98pub fn open_choice(at: PathPat, title: &str, icon: Icon, values: &[(&str, &str)]) -> FieldRule {
99    open_choice_terms(
100        at,
101        title,
102        icon,
103        values.iter().map(|(v, g)| term(v, g)).collect(),
104    )
105}
106
107/// [`open_choice`] over terms already built.
108pub fn open_choice_terms(at: PathPat, title: &str, icon: Icon, values: Vec<Term>) -> FieldRule {
109    FieldRule::new(at)
110        .ty(FieldType::Str)
111        .constraint(Constraint::Enum {
112            values,
113            closed: false,
114        })
115        .present(present(title, icon))
116}
117
118/// Mark a rule as one where *every* answer costs the same thing.
119///
120/// For the axes with no safe direction: whichever value you land on, the
121/// workspace gets rewritten. The tint is what draws the row; the
122/// [`Consequence`] is what Apply reads before carrying the change out.
123///
124/// Both, not either — they answer different questions. The tint says how
125/// loudly to draw a field the reader is *looking* at; the consequence says
126/// what happens if they go through with it. A row can be drawn calmly and
127/// still be expensive to change.
128pub fn costly(mut rule: FieldRule, tint: Tint, why: &str) -> FieldRule {
129    let present = std::mem::take(&mut rule.present);
130    rule.present = present.tint(tint).description(why);
131    rule.on_change(Consequence::always(why).severity(Severity::Confirm))
132}
133
134/// Mark one *answer* as the costly one.
135///
136/// The shape most of a workspace config actually has: `recycle_bin` is not
137/// dangerous, `recycle_bin: false` is; `fields.*.reify` is not expensive,
138/// turning it *on* is. Marking the field would warn on the harmless answer too,
139/// and a warning that fires either way is how a reader learns to click through
140/// warnings.
141///
142/// No tint, deliberately. A tint is a property of the row, and the row is not
143/// dangerous — one of its answers is. The warning belongs to the moment the
144/// answer is chosen, which is what the consequence carries.
145pub fn costly_when(
146    rule: FieldRule,
147    value: impl Into<Value>,
148    severity: Severity,
149    why: &str,
150) -> FieldRule {
151    rule.on_change(Consequence::when(value, why).severity(severity))
152}