morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
document.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <array>
6#include <cstddef>
7#include <cstdint>
8#include <deque>
9#include <expected>
10#include <format>
11#include <functional>
12#include <initializer_list>
13#include <map>
14#include <memory>
15#include <optional>
16#include <ranges>
17#include <set>
18#include <span>
19#include <stdexcept>
20#include <string>
21#include <string_view>
22#include <utility>
23#include <vector>
24
25#include "expression.hpp"
26#include "value.hpp"
27
28/// @file
29/// @brief The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and
30/// customization points — and `loadDocument`, which parses a document and runs its load-time validation.
31///
32/// Specified in `docs/spec/ui/document.md`.
33
34namespace morph::ui {
35
36/// @brief The vocabulary a document declares; the only one this client reads.
37inline constexpr std::string_view kVocabulary = "ui/1";
38
39/// @brief What the catalog says about a model type.
40struct ModelInfo {
41 /// @brief Whether a shared singleton is equivalent to a private instance.
42 bool stateless = false;
43 /// @brief Whether the model can be shared by an instance key, so an alias may give `instance` an expression.
44 bool keyed = false;
45};
46
47/// @brief One effect an action declares, as the catalog states it.
49 /// @brief What the effect is: `updates`, `changes`, `changesInstance` or `none`.
50 std::string kind;
51 /// @brief The wire name of the record the effect names; absent for `changesInstance` and `none`.
52 std::optional<std::string> record;
53
54 /// @brief Memberwise equality.
55 /// @param lhs One effect.
56 /// @param rhs The other.
57 /// @return True when kind and record match.
58 friend bool operator==(ActionEffect const& lhs, ActionEffect const& rhs) = default;
59};
60
61/// @brief A record an action's result holds, as the catalog states it.
63 /// @brief The record's wire name.
64 std::string record;
65 /// @brief The record's key field; absent for a keyless record.
66 std::optional<std::string> keyField;
67 /// @brief The JSON paths at which the result holds the record; `""` is the result itself.
68 std::vector<std::string> paths;
69
70 /// @brief Memberwise equality.
71 /// @param lhs One entry.
72 /// @param rhs The other.
73 /// @return True when record, key field and paths match.
74 friend bool operator==(ActionRecordInfo const& lhs, ActionRecordInfo const& rhs) = default;
75};
76
77/// @brief What the catalog says about an action.
78struct ActionInfo {
79 /// @brief The registered type id of the model the action belongs to.
80 std::string modelType;
81 /// @brief The fields of the action's reply rows that the server calculates; an `optimistic` write may not name
82 /// them.
83 std::vector<std::string> calculatedFields;
84 /// @brief The reply fields the server marks `secret`.
85 std::vector<std::string> secretFields;
86 /// @brief Whether the action sends a credential.
87 bool credential = false;
88 /// @brief When the action's result is a record, the names of its members that hold a list: what a `patch` or
89 /// `optimistic` of a query of the action may name as its `at`. Empty when the catalog does not say, or the
90 /// result is not a record.
91 std::optional<std::vector<std::string>> listFields;
92 /// @brief What the action declares it changes. Absent when it declared nothing, which makes it a read; present
93 /// and empty is not produced by the registry.
94 std::optional<std::vector<ActionEffect>> effects;
95 /// @brief The records the action's result holds, with the paths to each. Empty when it holds none or the
96 /// catalog does not say.
97 std::vector<ActionRecordInfo> records;
98};
99
100/// @brief The facts load-time validation needs from the application's catalog. Local mode answers them from the
101/// model registry in-process, the server at registration, a remote client from the UI service's catalog.
102///
103/// A lookup left empty skips the rules that need it: `model` the unknown-model and keyed-instance rules; `action`
104/// the unknown-action, wrong-model, optimistic and offline rules.
105struct Catalog {
106 /// @brief Looks up a model type by its registered type id.
107 std::function<std::optional<ModelInfo>(std::string_view typeId)> model;
108 /// @brief Looks up an action by its id.
109 std::function<std::optional<ActionInfo>(std::string_view actionId)> action;
110 /// @brief The schema JSON of an action, as `forms::schemaJson<A>()` emits it: what a `form` node builds its form
111 /// from. Local mode answers it from the action registry, a remote client from the server's `schemas`.
112 /// Empty: a `form` node is reported and shown as a muted text.
113 std::function<std::optional<std::string>(std::string_view actionId)> schema;
114 /// @brief The `v-*` view document whose `v-query` is an action, as `views::viewSchemaJson<V>()` emits it: what a
115 /// `collection` node naming that action builds its collection from. Empty: a `collection` node is
116 /// reported and shown as a muted text.
117 std::function<std::optional<std::string>(std::string_view actionId)> view;
118 /// @brief The `w-*` wizard document whose first step is an action, as `flows::wizardSchemaJson<W>()` emits it:
119 /// what a `wizard` node naming that action builds its wizard from. Empty: a `wizard` node is reported and
120 /// shown as a muted text.
121 std::function<std::optional<std::string>(std::string_view actionId)> wizard;
122};
123
124/// @brief A `Catalog::view` or `Catalog::wizard` lookup over documents by action id.
125/// @param documents The documents by action id; the lookup keeps its own copy.
126/// @return The lookup.
127[[nodiscard]] inline std::function<std::optional<std::string>(std::string_view)> documentLookup(
128 std::map<std::string, std::string, std::less<>> documents) {
129 return [known = std::make_shared<std::map<std::string, std::string, std::less<>> const>(std::move(documents))](
130 std::string_view actionId) -> std::optional<std::string> {
131 auto const found = known->find(actionId);
132 if (found == known->end()) {
133 return std::nullopt;
134 }
135 return found->second;
136 };
137}
138
139/// @brief Options of `loadDocument`.
141 /// @brief The catalog facts.
143};
144
145/// @brief How a model alias binds its instance (spec 5 §5).
146enum class InstanceMode : std::uint8_t {
147 Default, ///< None named: `shared` when the catalog marks the model stateless, `private` otherwise.
148 Private, ///< An instance of the scope's own, released when the scope unmounts.
149 Shared, ///< A stateless model's instance, its queries cached with other screens'; refused for a stateful model.
150 Keyed, ///< The shared instance an expression's key selects.
151 Borrowed, ///< The opener's binding of an alias (`{"from": alias}`), for a screen opened as a dialog.
152};
153
154/// @brief A model alias.
155struct ModelDecl {
156 /// @brief The alias.
157 std::string name;
158 /// @brief The declaration's JSON Pointer.
159 std::string path;
160 /// @brief The model's registered type id.
161 std::string type;
162 /// @brief How the instance binds.
164 /// @brief The key expression of a `Keyed` instance.
166 /// @brief The opener's alias of a `Borrowed` instance.
167 std::string borrowedFrom;
168};
169
170/// @brief A state declaration.
171struct StateDecl {
172 /// @brief The name.
173 std::string name;
174 /// @brief The declaration's JSON Pointer.
175 std::string path;
176 /// @brief The declared type; `any` when the document gives none.
178 /// @brief The initial value; `nullptr` starts `null`.
180};
181
182/// @brief A derived value (`let`).
183struct LetDecl {
184 /// @brief The name.
185 std::string name;
186 /// @brief The declaration's JSON Pointer.
187 std::string path;
188 /// @brief The expression.
190};
191
192/// @brief A query (spec 5 §5).
193struct QueryDecl {
194 /// @brief The name.
195 std::string name;
196 /// @brief The declaration's JSON Pointer.
197 std::string path;
198 /// @brief The model alias it goes through.
199 std::string model;
200 /// @brief The action id it runs, or evaluates.
201 std::string action;
202 /// @brief Whether it is an `evaluate` of the action (spec 5 §7) rather than a call.
203 bool evaluate = false;
204 /// @brief The body, which is the query's key; `nullptr` sends an empty object.
206 /// @brief The gate; `nullptr` is always on.
208 /// @brief Milliseconds the key must be stable before a fetch.
209 std::int64_t debounce = 0;
210 /// @brief Milliseconds between polls; 0 does not poll.
211 std::int64_t refreshEvery = 0;
212 /// @brief Milliseconds a cached value counts as fresh.
213 std::int64_t staleTime = 0;
214};
215
216/// @brief What a mutation run does while another is in flight.
217enum class Concurrency : std::uint8_t {
218 Exclusive, ///< The run is refused.
219 Serial, ///< Runs queue, one queue per `serialKey`.
220 Latest, ///< Every run is sent; only the newest reply applies.
221 Parallel, ///< Every run is sent; replies apply as they arrive.
222};
223
224/// @brief An `optimistic` write: the row a mutation run predicts before its reply.
226 /// @brief The query whose cached list it writes.
227 std::string query;
228 /// @brief The member of the query's record value that holds the list; empty when the value is the list itself.
229 std::string at;
230 /// @brief The row key to write, read against the run's body bound as `body`.
232 /// @brief The predicted row, read against the run's body bound as `body`.
234};
235
236struct Command;
237
238/// @brief A command list, run as one batch.
239using Commands = std::vector<Command>;
240
241/// @brief The commands of `ui/1` (spec 5 §8, spec 6).
242enum class CommandKind : std::uint8_t {
243 Set, ///< `{"set": "name.path", "value": e}`
244 Reset, ///< `{"reset": "name"}`
245 Run, ///< `{"run": "m", "body": e?, "as": "x"?, "each": e?}`
246 Refetch, ///< `{"refetch": "q"}`
247 Open, ///< `{"open": "dialog"}`
248 Close, ///< `{"close": "dialog" | "self"}`
249 Navigate, ///< `{"navigate": "screen" | {"route": e}, "params": e, "as": ..., "onResult": [...], ...}`
250 Return, ///< `{"return": e}`
251 Focus, ///< `{"focus": "id"}`
252 Host, ///< `{"host": "name", "args": e?}`
253 SwitchBackend, ///< `{"switchBackend": "name"}`
254 ResetPrefs, ///< `{"resetPrefs": "point" | "all"}`
255 SetLocale, ///< `{"setLocale": "de"}`
256 Notify, ///< `{"notify": e, "role": "success"}`
257 Seq, ///< `{"seq": [...]}`
258 All, ///< `{"all": [...]}`
259 If, ///< `{"if": [cond, [...], [...]]}`
260 CancelClose, ///< `{"cancelClose": true}`
261 Retry, ///< `{"retry": true}`
262 SignIn, ///< `{"signIn": {"from": "m"}}`
263 SignOut, ///< `{"signOut": true}`
264 Upload, ///< `{"upload": e, "to": "m", "as": "x"?, "id": "t"?, "into": "bytes"?, "nameAs"?, "typeAs"?}`
265 /// `{"download": e, "save": "dialog", "name": e?}` or `{"download": e, "to": "preview", "as": "url"}`, either
266 /// with `"from": "bytes"`, a `name` and a `type`
268};
269
270/// @brief One command. Which members a kind uses is listed with each member.
271struct Command {
272 /// @brief The kind.
274 /// @brief The command's JSON Pointer.
275 std::string path;
276 /// @brief The name it acts on: `set`'s `name.path`; the state of `reset`; the mutation of `run`, `signIn` and
277 /// `upload`; the query of `refetch`; the element of `open`, `close` and `focus`; the screen of
278 /// `navigate`; the name of `host`, `switchBackend`, `setLocale`; the point of `resetPrefs`; the state of
279 /// a `download` to preview.
280 std::string target;
281 /// @brief The main expression: `set`'s value, `run`'s body, `navigate`'s params, `return`'s value, `host`'s
282 /// args, `notify`'s message, `if`'s condition, the file of `upload` and `download`.
284 /// @brief The second expression: `run`'s `each`, `navigate`'s `route`, `download`'s suggested `name`.
286 /// @brief A binding name: `run`'s and `upload`'s `as`.
287 std::string binding;
288 /// @brief A closed option: `navigate`'s `as`, `notify`'s `role`, `download`'s `save` or `to`.
289 std::string option;
290 /// @brief The transfer id of `upload`, whose progress is an element value.
291 std::string transfer;
292 /// @brief Whether the file moves inline: `download`'s `"from": "bytes"`, `upload`'s `"into": "bytes"`.
293 bool bytes = false;
294 /// @brief `download`'s media type of inline bytes.
296 /// @brief `upload`'s body members for an inline file's name and media type; empty for none.
297 std::string nameAs;
298 /// @brief See `nameAs`.
299 std::string typeAs;
300 /// @brief Nested commands: `seq` and `all`; `if`'s then branch; `navigate`'s `onResult`.
302 /// @brief The alternative: `if`'s else branch; `navigate`'s `onCancel`.
304};
305
306/// @brief A mutation (spec 5 §5).
308 /// @brief The name.
309 std::string name;
310 /// @brief The declaration's JSON Pointer.
311 std::string path;
312 /// @brief The model alias it goes through.
313 std::string model;
314 /// @brief The action id it runs.
315 std::string action;
316 /// @brief The body; `nullptr` sends an empty object.
318 /// @brief What a run does while another is in flight.
320 /// @brief The queue key of `serial` runs.
322 /// @brief The row a run predicts before its reply.
323 std::optional<OptimisticDecl> optimistic;
324 /// @brief Commands run after a success.
326 /// @brief Commands run after a failure.
328 /// @brief Whether a run that cannot reach the server is queued offline.
329 bool offlineQueue = false;
330};
331
332/// @brief Which changes a watch reacts to.
333enum class WatchSource : std::uint8_t {
334 Any, ///< Every change.
335 User, ///< Only changes caused by the user's own edits.
336};
337
338/// @brief A cascade: commands that run when a value changes (spec 5 §6).
339struct WatchDecl {
340 /// @brief The declaration's JSON Pointer.
341 std::string path;
342 /// @brief The value watched.
344 /// @brief Which changes run it.
346 /// @brief The commands.
348};
349
350/// @brief A validation across elements (spec 5 §6).
351struct CheckDecl {
352 /// @brief The check's id.
353 std::string id;
354 /// @brief The declaration's JSON Pointer.
355 std::string path;
356 /// @brief The condition; `false` fails the check, `null` does not.
358 /// @brief The message added to each target's errors.
360 /// @brief The elements the message attaches to.
361 std::vector<std::string> targets;
362 /// @brief Whether a failure blocks `ready`.
363 bool blocking = false;
364};
365
366/// @brief The declarations of one scope, in declaration order.
367struct ScopeDecl {
368 /// @brief Model aliases.
369 std::vector<ModelDecl> models;
370 /// @brief State.
371 std::vector<StateDecl> state;
372 /// @brief Derived values.
373 std::vector<LetDecl> lets;
374 /// @brief Queries.
375 std::vector<QueryDecl> queries;
376 /// @brief Mutations.
377 std::vector<MutationDecl> mutations;
378 /// @brief Watches.
379 std::vector<WatchDecl> watches;
380 /// @brief Checks.
381 std::vector<CheckDecl> checks;
382};
383
384struct DocNode;
385
386/// @brief A shared, immutable view node.
387using DocNodePtr = std::shared_ptr<DocNode const>;
388
389/// @brief A view node of the document (spec 5 §9), or a structured part of one (a table column, a tab, a switch
390/// case, a menu item, an `a11y`, `drag`, `drop` or `action` record, a form field overlay).
391///
392/// Properties are kept by their document keys, so the interpreter reads a kind's properties by name; which keys a
393/// kind has, and what shape each takes, is the kind table of `docs/spec/ui/document.md`.
394struct DocNode {
395 /// @brief The node kind (`text`, `table`, …), or for a part its part kind (`column`, `tab`, `case`, …).
396 std::string kind;
397 /// @brief The element id; empty when none is given.
398 std::string id;
399 /// @brief The node's JSON Pointer.
400 std::string path;
401 /// @brief Expression properties, by key.
402 std::map<std::string, ExprPtr, std::less<>> props;
403 /// @brief Command lists, by key (`onClick`, …), including a custom node's `on` events as `on.<event>`.
404 std::map<std::string, Commands, std::less<>> events;
405 /// @brief Child node lists, by key (`children`, `header`, `loading`, `error`, `default`, `cell`, `fallback`).
406 std::map<std::string, std::vector<DocNodePtr>, std::less<>> children;
407 /// @brief Structured parts, by key (`columns`, `tabs`, `cases`, `items`, `a11y`, `action`, `fields`, …).
408 std::map<std::string, std::vector<DocNodePtr>, std::less<>> parts;
409 /// @brief Plain string properties, by key (`testId`, `surface`, `name` of a custom node, `model` and `action`
410 /// of a form, `point` of `customize`, `role` of `a11y`, a column's `id`, …).
411 std::map<std::string, std::string, std::less<>> names;
412 /// @brief Key bindings: chord to commands.
413 std::map<std::string, Commands, std::less<>> keys;
414 /// @brief Literal flags, by key (`showLabel` of an input).
415 std::map<std::string, bool, std::less<>> flags;
416 /// @brief The declarations of the scope the node creates (dialog, drawer, switch case, `forEach` or `table`
417 /// row); empty for a node that creates none.
418 std::optional<ScopeDecl> scope;
419
420 /// @brief An expression property.
421 /// @param key The key.
422 /// @return The expression, or `nullptr` when absent.
423 [[nodiscard]] ExprPtr prop(std::string_view key) const {
424 auto const found = props.find(key);
425 return found == props.end() ? nullptr : found->second;
426 }
427
428 /// @brief A command list.
429 /// @param key The key.
430 /// @return The commands, or `nullptr` when absent.
431 [[nodiscard]] Commands const* event(std::string_view key) const {
432 auto const found = events.find(key);
433 return found == events.end() ? nullptr : &found->second;
434 }
435
436 /// @brief A child node list.
437 /// @param key The key.
438 /// @return The nodes; empty when absent.
439 [[nodiscard]] std::span<DocNodePtr const> nodes(std::string_view key) const {
440 auto const found = children.find(key);
441 return found == children.end() ? std::span<DocNodePtr const>{} : std::span<DocNodePtr const>{found->second};
442 }
443
444 /// @brief A list of parts.
445 /// @param key The key.
446 /// @return The parts; empty when absent.
447 [[nodiscard]] std::span<DocNodePtr const> partList(std::string_view key) const {
448 auto const found = parts.find(key);
449 return found == parts.end() ? std::span<DocNodePtr const>{} : std::span<DocNodePtr const>{found->second};
450 }
451
452 /// @brief A plain string property.
453 /// @param key The key.
454 /// @return The string; empty when absent.
455 [[nodiscard]] std::string_view name(std::string_view key) const {
456 auto const found = names.find(key);
457 return found == names.end() ? std::string_view{} : std::string_view{found->second};
458 }
459
460 /// @brief A literal flag.
461 /// @param key The key.
462 /// @return Its value; `false` when absent.
463 [[nodiscard]] bool flag(std::string_view key) const {
464 auto const found = flags.find(key);
465 return found != flags.end() && found->second;
466 }
467};
468
469/// @brief A custom component declaration (spec 5 §10).
470struct Component {
471 /// @brief The component's name.
472 std::string name;
473 /// @brief The declaration's JSON Pointer.
474 std::string path;
475 /// @brief The content hash of a delivered QML component (`sha256:<hex>`); empty for a host component.
476 std::string qml;
477 /// @brief The name of a host component; empty for a delivered one.
478 std::string host;
479 /// @brief The host component's version.
480 std::int64_t version = 0;
481 /// @brief The props and their prop types (`string`, `bool`, `int`, `exact`, `number`, `json`).
482 std::map<std::string, std::string, std::less<>> props;
483 /// @brief The events and their payload types.
484 std::map<std::string, Type, std::less<>> events;
485 /// @brief The subtree of built-in nodes a renderer shows when it cannot load the component.
487};
488
489/// @brief A customization point (spec 5 §12b).
491 /// @brief The point's name.
492 std::string name;
493 /// @brief The declaration's JSON Pointer.
494 std::string path;
495 /// @brief `columns`, `choice`, `value`, `layout`, `saved`, `arrange`, and in the app document `pins`.
496 std::string kind;
497 /// @brief The element a `columns` or `arrange` point adjusts.
498 std::string of;
499 /// @brief The options of a `choice`.
500 std::vector<std::string> options;
501 /// @brief The type of a `value`.
503 /// @brief The default choice; `null` when none is declared.
505};
506
507/// @brief What a load-time warning is about. A warning never refuses a document.
508enum class LoadWarningKind : std::uint8_t {
509 WatchFeedback, ///< A watch of a query runs a mutation that changes data, on any change of it.
510 Uncovered, ///< A mutation whose action declares nothing, and after which nothing refreshes.
511};
512
513/// @brief A finding of load-time validation that does not refuse the document.
515 /// @brief What it is about.
517 /// @brief The JSON Pointer of the declaration it names.
518 std::string path;
519 /// @brief The finding, in one sentence.
520 std::string text;
521
522 /// @brief The warning as one line of text.
523 /// @param document The id of the document it was found in; empty leaves it out.
524 /// @return `document <id> at <path>: <text>`, leaving out the parts that are empty.
525 [[nodiscard]] std::string message(std::string_view document) const {
526 std::string out;
527 if (!document.empty()) {
528 out += "document " + std::string{document} + " ";
529 }
530 out += "at " + (path.empty() ? std::string{"/"} : path) + ": " + text;
531 return out;
532 }
533};
534
535/// @brief A loaded and validated UI document: a screen, or the app document (id `app`, spec 6 §5).
536struct Document {
537 /// @brief The vocabulary, `ui/1`.
538 std::string vocab;
539 /// @brief The document id, such as `lims.results`.
540 std::string id;
541 /// @brief The title expression.
543 /// @brief The identity expression: equal identities are one screen.
545 /// @brief The unsaved-changes expression.
547 /// @brief The subtitle expression of the screen's head; `nullptr` when none is declared.
549 /// @brief The head's status badges: `headBadge` parts, in order.
550 std::vector<DocNodePtr> status;
551 /// @brief The head's actions: `actionButton` parts, in order.
552 std::vector<DocNodePtr> actions;
553 /// @brief The typed params, in declaration order.
554 std::vector<std::pair<std::string, Type>> params;
555 /// @brief The screen scope's declarations.
557 /// @brief The lifecycle command lists by key: `onMount`, `onUnmount`, `onClose`, `onSave`, `onBackendChange`.
558 std::map<std::string, Commands, std::less<>> lifecycle;
559 /// @brief The screen's key bindings.
560 std::map<std::string, Commands, std::less<>> keys;
561 /// @brief When errors show: `touched`, `submit` or `always`; empty for the default.
562 std::string showErrors;
563 /// @brief Queries started before the screen mounts.
564 std::vector<std::string> prefetch;
565 /// @brief Custom components, by name.
566 std::map<std::string, Component, std::less<>> components;
567 /// @brief Customization points, by name.
568 std::map<std::string, CustomizePoint, std::less<>> customize;
569 /// @brief The view; `nullptr` for the app document.
571 /// @brief The static type of each `dateTimeInput`'s `value` that the loader could decide: `String` or
572 /// `Timestamp`, keyed by the node. A node whose value type is `any` or `key` has no entry.
573 std::map<DocNode const*, TypeKind> dateValueKinds;
574 /// @brief What load-time validation found that does not refuse the document, in the order found. Empty when
575 /// the catalog answers no actions.
576 std::vector<LoadWarning> warnings;
577};
578
579namespace detail {
580
581/// @brief The exception the loader throws inside itself; `loadDocument` turns it into its result.
582class DocumentRefused : public std::runtime_error {
583public:
584 /// @brief Wraps a refusal.
585 /// @param refusal The refusal.
586 explicit DocumentRefused(Refusal refusal) : std::runtime_error{refusal.reason}, _refusal{std::move(refusal)} {}
587
588 /// @brief The refusal.
589 /// @return It.
590 [[nodiscard]] Refusal const& refusal() const noexcept { return _refusal; }
591
592private:
593 Refusal _refusal;
594};
595
596/// @brief Refuses the document.
597/// @param path Where.
598/// @param reason Why.
599[[noreturn]] inline void refuse(std::string const& path, std::string reason) {
600 throw DocumentRefused{Refusal{.document = {}, .path = path, .reason = std::move(reason)}};
601}
602
603/// @brief What shape a node key's value takes.
604enum class Shape : std::uint8_t {
605 Expr, ///< An expression.
606 Condition, ///< An expression checked as a `bool`.
607 Commands, ///< A command list.
608 Nodes, ///< A node or a list of nodes.
609 Text, ///< A plain string.
610 Name, ///< A plain string that is a name.
611 Part, ///< A record, parsed by the part table of its part kind.
612 Parts, ///< A list of such records.
613 PartsOrExpr, ///< A list of records, or an expression yielding one.
614 ExprRecord, ///< A record of expressions.
615 CommandRecord, ///< A record of command lists.
616 Keys, ///< Key bindings: chord to command list.
617 Flag, ///< A literal `true` or `false`, set once.
618};
619
620/// @brief The key is evaluated in the node's own scope (a dialog's, a row's), not the scope around it.
621inline constexpr std::uint8_t kInner = 1U;
622/// @brief The key is required.
623inline constexpr std::uint8_t kRequired = 2U;
624/// @brief The key's content is a boundary's `error`, where `boundary.error` and `retry` are valid.
625inline constexpr std::uint8_t kBoundaryError = 4U;
626
627/// @brief One key of a kind.
628struct KeySpec {
629 /// @brief The key.
630 std::string_view key;
631 /// @brief Its shape.
633 /// @brief The part kind, for `Part`, `Parts` and `PartsOrExpr`.
634 std::string_view part = {}; // NOLINT(readability-redundant-member-init): table rows leave it out
635 /// @brief `kInner`, `kRequired`, `kBoundaryError`.
636 std::uint8_t flags = 0;
637};
638
639/// @brief The node or part creates a scope.
640inline constexpr std::uint8_t kScope = 1U;
641/// @brief The scope it creates is a row scope, where `row` is valid.
642inline constexpr std::uint8_t kRowScope = 2U;
643/// @brief It carries the input decorations `readonly`, `required`, `errors` and `stale`.
644inline constexpr std::uint8_t kField = 4U;
645/// @brief It is a part, not a node: it has no common properties.
646inline constexpr std::uint8_t kPart = 8U;
647/// @brief A command may write its draft with `set`.
648inline constexpr std::uint8_t kWritable = 16U;
649/// @brief Any path below its id is an element value (a form's fields).
650inline constexpr std::uint8_t kOpenValues = 32U;
651
652/// @brief A node kind or part kind: its keys and its element values.
653struct KindSpec {
654 /// @brief The kind.
655 std::string_view kind;
656 /// @brief Its own keys.
657 std::span<KeySpec const> keys;
658 /// @brief `kScope`, `kRowScope`, `kField`, `kPart`, `kWritable`, `kOpenValues`.
659 std::uint8_t flags = 0;
660 /// @brief The element values beyond those every element has.
661 std::span<std::string_view const> values = {}; // NOLINT(readability-redundant-member-init): rows leave it out
662};
663
664// NOLINTBEGIN(modernize-use-designated-initializers): the kind table reads as rows; names on every field would not.
665/// @brief The keys every node takes.
666inline constexpr std::array<KeySpec, 18> kCommonKeys = {{
667 {"id", Shape::Name},
668 {"visible", Shape::Condition},
669 {"enabled", Shape::Condition},
670 {"tooltip", Shape::Expr},
671 {"sizing", Shape::Expr},
672 {"minWidth", Shape::Expr},
673 {"maxWidth", Shape::Expr},
674 {"minHeight", Shape::Expr},
675 {"maxHeight", Shape::Expr},
676 {"alignSelf", Shape::Text},
677 {"span", Shape::Expr},
678 {"testId", Shape::Text},
679 {"surface", Shape::Text},
680 {"autofocus", Shape::Expr},
681 {"a11y", Shape::Part, "a11y"},
682 {"drag", Shape::Part, "drag"},
683 {"drop", Shape::Part, "drop"},
684 {"keys", Shape::Keys},
685}};
686
687/// @brief The input decorations text and the inputs take.
688inline constexpr std::array<KeySpec, 4> kFieldKeys = {{
689 {"readonly", Shape::Condition},
690 {"required", Shape::Condition},
691 {"errors", Shape::Expr},
692 {"stale", Shape::Condition},
693}};
694
695/// @brief The element values every element has.
696inline constexpr std::array<std::string_view, 5> kAnyValues = {"visible", "enabled", "valid", "errors", "shownErrors"};
697/// @brief The element values of a text, date-time or slider input.
698inline constexpr std::array<std::string_view, 5> kInputValues = {"value", "draft", "dirty", "focused", "touched"};
699/// @brief The element values of a checkbox.
700inline constexpr std::array<std::string_view, 1> kCheckboxValues = {"checked"};
701/// @brief The element values of a select.
702inline constexpr std::array<std::string_view, 2> kSelectValues = {"selected", "selectedOption"};
703/// @brief The element values of a table.
704inline constexpr std::array<std::string_view, 10> kTableValues = {
705 "selection", "activeKey", "sort", "filters", "viewCount",
706 "sourceCount", "selectedCount", "pending", "error", "hoveredKey"};
707/// @brief The element values of a dialog or drawer.
708inline constexpr std::array<std::string_view, 3> kDialogValues = {"open", "ready", "submitted"};
709/// @brief The element values of a form, beside its fields.
710inline constexpr std::array<std::string_view, 3> kFormValues = {"ready", "body", "submitted"};
711/// @brief The values of a transfer id.
712inline constexpr std::array<std::string_view, 3> kTransferValues = {"progress", "pending", "error"};
713
714/// @brief The keys of `text`.
715inline constexpr std::array<KeySpec, 2> kTextKeys = {{{"text", Shape::Expr}, {"role", Shape::Expr}}};
716/// @brief The keys of `button`.
717inline constexpr std::array<KeySpec, 7> kButtonKeys = {{{"label", Shape::Expr},
718 {"icon", Shape::Expr},
719 {"emphasis", Shape::Text},
720 {"style", Shape::Text},
721 {"selected", Shape::Condition},
722 {"onClick", Shape::Commands},
723 {"onActivate", Shape::Commands}}};
724/// @brief The keys of `textInput`.
725inline constexpr std::array<KeySpec, 10> kTextInputKeys = {{{"value", Shape::Expr},
726 {"placeholder", Shape::Expr},
727 {"mode", Shape::Expr},
728 {"icon", Shape::Expr},
729 {"commitOn", Shape::Text},
730 {"label", Shape::Expr},
731 {"showLabel", Shape::Flag},
732 {"onChange", Shape::Commands},
733 {"onCommit", Shape::Commands},
734 {"onSubmit", Shape::Commands}}};
735/// @brief The keys of `checkbox`.
736inline constexpr std::array<KeySpec, 4> kCheckboxKeys = {
737 {{"label", Shape::Expr}, {"checked", Shape::Expr}, {"onToggle", Shape::Commands}, {"onChange", Shape::Commands}}};
738/// @brief The keys of `select`.
739inline constexpr std::array<KeySpec, 8> kSelectKeys = {{{"options", Shape::Expr},
740 {"selected", Shape::Expr},
741 {"style", Shape::Expr},
742 {"label", Shape::Expr},
743 {"showLabel", Shape::Flag},
744 {"onStale", Shape::Text},
745 {"onChoose", Shape::Commands},
746 {"onChange", Shape::Commands}}};
747/// @brief The keys of `menu`.
748inline constexpr std::array<KeySpec, 1> kMenuKeys = {{{"items", Shape::Parts, "menuItem"}}};
749/// @brief The keys of a menu item part.
750inline constexpr std::array<KeySpec, 7> kMenuItemKeys = {{{"label", Shape::Expr},
751 {"icon", Shape::Expr},
752 {"keys", Shape::Text},
753 {"checked", Shape::Expr},
754 {"enabled", Shape::Condition},
755 {"onClick", Shape::Commands},
756 {"items", Shape::Parts, "menuItem"}}};
757/// @brief The keys of `column` and `row`.
758inline constexpr std::array<KeySpec, 4> kContainerKeys = {
759 {{"children", Shape::Nodes}, {"gap", Shape::Expr}, {"align", Shape::Text}, {"justify", Shape::Text}}};
760/// @brief The keys of `grid`; `justify` is read only to refuse it.
761inline constexpr std::array<KeySpec, 5> kGridKeys = {{{"children", Shape::Nodes},
762 {"gap", Shape::Expr},
763 {"columns", Shape::Expr},
764 {"align", Shape::Text},
765 {"justify", Shape::Text}}};
766/// @brief The keys of `panel`.
767inline constexpr std::array<KeySpec, 7> kPanelKeys = {{{"title", Shape::Expr},
768 {"header", Shape::Nodes},
769 {"padding", Shape::Expr},
770 {"collapsible", Shape::Expr},
771 {"collapsed", Shape::Expr},
772 {"onToggle", Shape::Commands},
773 {"children", Shape::Nodes}}};
774/// @brief The keys of `scroll`.
775inline constexpr std::array<KeySpec, 2> kScrollKeys = {{{"axis", Shape::Expr}, {"children", Shape::Nodes}}};
776/// @brief The keys of `switch`.
777inline constexpr std::array<KeySpec, 3> kSwitchKeys = {
778 {{"on", Shape::Expr, {}, kRequired}, {"cases", Shape::Parts, "case"}, {"default", Shape::Nodes}}};
779/// @brief The keys of a `switch` case part.
780inline constexpr std::array<KeySpec, 2> kCaseKeys = {
781 {{"when", Shape::Expr, {}, kRequired}, {"children", Shape::Nodes, {}, kInner}}};
782/// @brief The keys of `tabs`.
783inline constexpr std::array<KeySpec, 1> kTabsKeys = {{{"tabs", Shape::Parts, "tab"}}};
784/// @brief The keys of a `tabs` page part.
785inline constexpr std::array<KeySpec, 2> kTabKeys = {{{"label", Shape::Expr}, {"children", Shape::Nodes}}};
786/// @brief The keys of `dialog` and `drawer`.
787inline constexpr std::array<KeySpec, 9> kDialogKeys = {{{"open", Shape::Condition},
788 {"title", Shape::Expr},
789 {"showErrors", Shape::Text},
790 {"children", Shape::Nodes, {}, kInner},
791 {"onDismiss", Shape::Commands},
792 {"default", Shape::Commands, {}, kInner},
793 {"cancel", Shape::Commands, {}, kInner},
794 {"actions", Shape::Parts, "actionButton", kInner},
795 {"side", Shape::Expr}}};
796/// @brief The keys of `busy`.
797inline constexpr std::array<KeySpec, 2> kBusyKeys = {{{"active", Shape::Condition}, {"label", Shape::Expr}}};
798/// @brief The keys of `forEach`.
799inline constexpr std::array<KeySpec, 5> kForEachKeys = {{{"rows", Shape::Expr, {}, kRequired},
800 {"key", Shape::Expr, {}, kInner},
801 {"axis", Shape::Expr},
802 {"gap", Shape::Expr},
803 {"children", Shape::Nodes, {}, kInner}}};
804/// @brief The keys of `table`.
805inline constexpr std::array<KeySpec, 18> kTableKeys = {{{"rows", Shape::Expr},
806 {"key", Shape::Expr, {}, kInner},
807 {"model", Shape::Name},
808 {"action", Shape::Text},
809 {"body", Shape::Expr},
810 {"mode", Shape::Expr},
811 {"columns", Shape::PartsOrExpr, "column"},
812 {"showHeader", Shape::Flag},
813 {"virtualizeThreshold", Shape::Expr},
814 {"virtualizeOverscan", Shape::Expr},
815 {"selectionMode", Shape::Expr},
816 {"selection", Shape::Expr},
817 {"sort", Shape::Expr},
818 {"filters", Shape::Expr},
819 {"rowTone", Shape::Expr, {}, kInner},
820 {"highlightKey", Shape::Expr},
821 {"onActivate", Shape::Commands, {}, kInner},
822 {"empty", Shape::Nodes}}};
823/// @brief The keys of a `table` column part.
824inline constexpr std::array<KeySpec, 5> kColumnKeys = {{{"id", Shape::Name},
825 {"label", Shape::Expr},
826 {"width", Shape::Expr},
827 {"role", Shape::Expr},
828 {"cell", Shape::Nodes, {}, kInner}}};
829/// @brief The keys of `dateTimeInput`.
830inline constexpr std::array<KeySpec, 6> kDateTimeKeys = {{{"value", Shape::Expr},
831 {"mode", Shape::Expr},
832 {"label", Shape::Expr},
833 {"showLabel", Shape::Flag},
834 {"onChange", Shape::Commands},
835 {"onCommit", Shape::Commands}}};
836/// @brief The keys of `slider`.
837inline constexpr std::array<KeySpec, 7> kSliderKeys = {{{"value", Shape::Expr},
838 {"min", Shape::Expr},
839 {"max", Shape::Expr},
840 {"step", Shape::Expr},
841 {"label", Shape::Expr},
842 {"onChange", Shape::Commands},
843 {"onCommit", Shape::Commands}}};
844/// @brief The keys of `filePicker`.
845inline constexpr std::array<KeySpec, 5> kFilePickerKeys = {{{"accept", Shape::Expr},
846 {"multiple", Shape::Expr},
847 {"mode", Shape::Expr},
848 {"label", Shape::Expr},
849 {"onPick", Shape::Commands}}};
850/// @brief The keys of `banner`.
851inline constexpr std::array<KeySpec, 5> kBannerKeys = {{{"tone", Shape::Expr},
852 {"text", Shape::Expr},
853 {"dismissible", Shape::Expr},
854 {"action", Shape::Part, "action"},
855 {"onDismiss", Shape::Commands}}};
856/// @brief The keys of an `action` part.
857inline constexpr std::array<KeySpec, 2> kActionKeys = {{{"label", Shape::Expr}, {"onClick", Shape::Commands}}};
858/// @brief The keys of an `actionButton` part: a button a screen's head or an overlay's footer shows.
859inline constexpr std::array<KeySpec, 7> kActionButtonKeys = {{{"id", Shape::Name, {}, kRequired},
860 {"label", Shape::Expr, {}, kRequired},
861 {"icon", Shape::Expr},
862 {"emphasis", Shape::Text},
863 {"enabled", Shape::Condition},
864 {"visible", Shape::Condition},
865 {"onClick", Shape::Commands}}};
866/// @brief The keys of a `headBadge` part: a status badge of a screen's head.
867inline constexpr std::array<KeySpec, 4> kHeadBadgeKeys = {
868 {{"tone", Shape::Expr}, {"text", Shape::Expr}, {"icon", Shape::Expr}, {"visible", Shape::Condition}}};
869/// @brief The keys of `badge`.
870inline constexpr std::array<KeySpec, 3> kBadgeKeys = {
871 {{"tone", Shape::Expr}, {"text", Shape::Expr}, {"icon", Shape::Expr}}};
872/// @brief The keys of `progress`.
873inline constexpr std::array<KeySpec, 2> kProgressKeys = {{{"value", Shape::Expr}, {"label", Shape::Expr}}};
874/// @brief The keys of `steps`.
875inline constexpr std::array<KeySpec, 2> kStepsKeys = {{{"items", Shape::Expr}, {"current", Shape::Expr}}};
876/// @brief The keys of `keyValue`.
877inline constexpr std::array<KeySpec, 1> kKeyValueKeys = {{{"items", Shape::Expr}}};
878/// @brief The keys of `emptyState`.
879inline constexpr std::array<KeySpec, 4> kEmptyStateKeys = {
880 {{"title", Shape::Expr}, {"text", Shape::Expr}, {"icon", Shape::Expr}, {"action", Shape::Part, "action"}}};
881/// @brief The keys of `splitter`.
882inline constexpr std::array<KeySpec, 3> kSplitterKeys = {
883 {{"orientation", Shape::Expr}, {"sizes", Shape::Expr}, {"children", Shape::Nodes}}};
884/// @brief The keys of `collapsible`.
885inline constexpr std::array<KeySpec, 5> kCollapsibleKeys = {{{"title", Shape::Expr},
886 {"open", Shape::Condition},
887 {"header", Shape::Nodes},
888 {"children", Shape::Nodes},
889 {"onToggle", Shape::Commands}}};
890/// @brief The keys of `dropZone`.
891inline constexpr std::array<KeySpec, 4> kDropZoneKeys = {
892 {{"accept", Shape::Expr}, {"multiple", Shape::Expr}, {"children", Shape::Nodes}, {"onDrop", Shape::Commands}}};
893/// @brief The keys of `customize`.
894inline constexpr std::array<KeySpec, 1> kCustomizeKeys = {{{"point", Shape::Name, {}, kRequired}}};
895/// @brief The keys of `boundary`.
896inline constexpr std::array<KeySpec, 3> kBoundaryKeys = {
897 {{"loading", Shape::Nodes}, {"error", Shape::Nodes, {}, kBoundaryError}, {"children", Shape::Nodes}}};
898/// @brief The keys of `form`.
899inline constexpr std::array<KeySpec, 15> kFormKeys = {{{"model", Shape::Name, {}, kRequired},
900 {"action", Shape::Text, {}, kRequired},
901 {"prefill", Shape::Expr},
902 {"key", Shape::Expr},
903 {"submit", Shape::Text},
904 {"showErrors", Shape::Text},
905 {"fields", Shape::ExprRecord, "fields"},
906 {"onSubmit", Shape::Commands},
907 {"onSuccess", Shape::Commands},
908 {"onError", Shape::Commands},
909 {"title", Shape::Expr},
910 {"submitLabel", Shape::Expr},
911 {"showStatus", Shape::Flag},
912 {"columns", Shape::Expr},
913 {"fieldLayout", Shape::Text}}};
914/// @brief The keys of `collection` and `wizard`: the model and action, and the presentation keys that reach every
915/// form the view draws.
916inline constexpr std::array<KeySpec, 7> kCollectionKeys = {{{"model", Shape::Name, {}, kRequired},
917 {"action", Shape::Text, {}, kRequired},
918 {"title", Shape::Expr},
919 {"submitLabel", Shape::Expr},
920 {"showStatus", Shape::Flag},
921 {"columns", Shape::Expr},
922 {"fieldLayout", Shape::Text}}};
923/// @brief The keys of `custom`.
924inline constexpr std::array<KeySpec, 3> kCustomKeys = {
925 {{"name", Shape::Text, {}, kRequired}, {"props", Shape::ExprRecord}, {"on", Shape::CommandRecord}}};
926/// @brief The keys of an `a11y` part.
927inline constexpr std::array<KeySpec, 2> kA11yKeys = {{{"name", Shape::Expr}, {"role", Shape::Text}}};
928/// @brief The keys of a `drag` part.
929inline constexpr std::array<KeySpec, 1> kDragKeys = {{{"payload", Shape::Expr, {}, kRequired}}};
930/// @brief The keys of a `drop` part.
931inline constexpr std::array<KeySpec, 2> kDropKeys = {{{"accepts", Shape::Expr}, {"onDrop", Shape::Commands}}};
932
933/// @brief The view node kinds of `ui/1` and the part kinds inside them.
934inline constexpr std::array<KindSpec, 48> kKinds = {{
935 {"text", kTextKeys, kField},
936 {"button", kButtonKeys},
937 {"textInput", kTextInputKeys, kField | kWritable, kInputValues},
940 {"menu", kMenuKeys},
941 {"column", kContainerKeys},
942 {"row", kContainerKeys},
943 {"grid", kGridKeys},
944 {"spacer", {}},
945 {"panel", kPanelKeys},
946 {"scroll", kScrollKeys},
947 {"switch", kSwitchKeys},
948 {"tabs", kTabsKeys},
949 {"dialog", kDialogKeys, kScope, kDialogValues},
950 {"drawer", kDialogKeys, kScope, kDialogValues},
951 {"busy", kBusyKeys},
952 {"forEach", kForEachKeys, kScope | kRowScope},
954 {"dateTimeInput", kDateTimeKeys, kField | kWritable, kInputValues},
956 {"filePicker", kFilePickerKeys, kField},
957 {"banner", kBannerKeys},
958 {"badge", kBadgeKeys},
959 {"progress", kProgressKeys},
960 {"steps", kStepsKeys},
961 {"keyValue", kKeyValueKeys},
962 {"emptyState", kEmptyStateKeys},
963 {"splitter", kSplitterKeys},
964 {"collapsible", kCollapsibleKeys},
965 {"dropZone", kDropZoneKeys},
966 {"customize", kCustomizeKeys},
967 {"boundary", kBoundaryKeys},
969 {"collection", kCollectionKeys},
970 {"wizard", kCollectionKeys},
971 {"custom", kCustomKeys},
972 {"menuItem", kMenuItemKeys, kPart},
973 {"case", kCaseKeys, kPart | kScope},
974 {"tab", kTabKeys, kPart},
975 {"column", kColumnKeys, kPart},
976 {"action", kActionKeys, kPart},
977 {"a11y", kA11yKeys, kPart},
978 {"drag", kDragKeys, kPart},
979 {"drop", kDropKeys, kPart},
980 {"actionButton", kActionButtonKeys, kPart},
981 {"headBadge", kHeadBadgeKeys, kPart},
982 {"fields", {}, kPart},
983}};
984// NOLINTEND(modernize-use-designated-initializers)
985
986/// @brief Looks up a node kind or a part kind.
987/// @param kind The kind.
988/// @param part Whether a part kind is wanted.
989/// @return The spec, or `nullptr`.
990[[nodiscard]] inline KindSpec const* findKind(std::string_view kind, bool part) {
991 for (KindSpec const& spec : kKinds) {
992 if (spec.kind == kind && ((spec.flags & kPart) != 0) == part) {
993 return &spec;
994 }
995 }
996 return nullptr;
997}
998
999/// @brief Whether a list of names holds one.
1000/// @param names The list.
1001/// @param name The candidate.
1002/// @return Whether @p name is in @p names.
1003[[nodiscard]] inline bool listed(std::span<std::string_view const> names, std::string_view name) {
1004 return std::ranges::find(names, name) != names.end();
1005}
1006
1007/// @brief Refuses a must-understand key (`!name`) the client does not know; other unknown keys are ignored.
1008/// @param object The JSON object.
1009/// @param path Its JSON Pointer.
1010inline void refuseMustUnderstand(Json::object_t const& object, std::string const& path) {
1011 for (auto const& [key, value] : object) {
1012 static_cast<void>(value);
1013 if (key.starts_with('!')) {
1014 refuse(pointerAppend(path, key), "the must-understand key `" + key + "` is not part of ui/1");
1015 }
1016 }
1017}
1018
1019/// @brief Parses a key chord, and refuses one the workspace shell reserves.
1020/// @param chord The chord, such as `Ctrl+Shift+K`.
1021/// @param path Its JSON Pointer.
1022// NOLINTNEXTLINE(readability-function-cognitive-complexity): a chord's grammar, modifiers then one key name.
1023inline void checkChord(std::string const& chord, std::string const& path) {
1024 static constexpr std::array<std::string_view, 4> kModifiers = {"Ctrl", "Shift", "Alt", "Meta"};
1025 static constexpr std::array<std::string_view, 9> kNamedKeys = {"Enter", "Esc", "Tab", "Up", "Down",
1026 "Left", "Right", "Space", "Delete"};
1027 std::vector<std::string_view> parts;
1028 std::size_t start = 0;
1029 for (std::size_t plus = chord.find('+'); plus != std::string::npos; plus = chord.find('+', start)) {
1030 parts.push_back(std::string_view{chord}.substr(start, plus - start));
1031 start = plus + 1;
1032 }
1033 std::string_view const key = std::string_view{chord}.substr(start);
1034 std::set<std::string_view> modifiers;
1035 for (std::string_view const modifier : parts) {
1036 if (!listed(kModifiers, modifier) || !modifiers.insert(modifier).second) {
1037 refuse(path, "`" + chord + "` is not a chord: modifiers are Ctrl, Shift, Alt and Meta, once each");
1038 }
1039 }
1040 bool const letterOrDigit =
1041 key.size() == 1 && ((key.front() >= 'A' && key.front() <= 'Z') || (key.front() >= 'a' && key.front() <= 'z') ||
1042 (key.front() >= '0' && key.front() <= '9'));
1043 bool function = key.size() >= 2 && key.size() <= 3 && key.front() == 'F';
1044 int number = 0;
1045 for (char const chr : key.substr(function ? 1 : key.size())) {
1046 function = function && chr >= '0' && chr <= '9';
1047 number = (number * 10) + (chr - '0');
1048 }
1049 function = function && number >= 1 && number <= 24;
1050 if (!letterOrDigit && !function && !listed(kNamedKeys, key)) {
1051 refuse(path, "`" + chord + "` does not end in a key name");
1052 }
1053 bool const ctrlOnly = modifiers.size() == 1 && modifiers.contains("Ctrl");
1054 bool const ctrlShift = modifiers.size() == 2 && modifiers.contains("Ctrl") && modifiers.contains("Shift");
1055 bool const reserved = (key == "Tab" && (ctrlOnly || ctrlShift)) ||
1056 (ctrlOnly && (key == "W" || key == "w" || key == "K" || key == "k"));
1057 if (reserved) {
1058 refuse(path, "`" + chord + "` is reserved for the workspace shell");
1059 }
1060}
1061
1062// NOLINTBEGIN(misc-no-recursion): a document is a tree of nodes and command lists, and parsing descends it.
1063/// @brief Turns the JSON of a document into its model, refusing what does not parse.
1065public:
1066 /// @brief Parses the whole document.
1067 /// @param json The document.
1068 /// @return The model, not yet checked against its names.
1069 [[nodiscard]] Document parse(Json const& json) {
1070 auto const* const object = std::get_if<Json::object_t>(&json.data);
1071 if (object == nullptr) {
1072 refuse("", "a document is a JSON object");
1073 }
1074 refuseMustUnderstand(*object, "");
1075 Document doc;
1076 doc.vocab = text(*object, "vocab", "", true);
1077 if (doc.vocab != kVocabulary) {
1078 refuse("/vocab", "the vocabulary `" + doc.vocab + "` is not supported; this client reads ui/1");
1079 }
1080 doc.id = text(*object, "id", "", true);
1081 if (doc.id.empty()) {
1082 refuse("/id", "a document has a non-empty id");
1083 }
1084 doc.title = optionalExpr(*object, "title", "");
1085 doc.identity = optionalExpr(*object, "identity", "");
1086 doc.dirty = optionalExpr(*object, "dirty", "");
1087 doc.subtitle = optionalExpr(*object, "subtitle", "");
1088 doc.status = topParts(*object, "status", "headBadge");
1089 doc.actions = topParts(*object, "actions", "actionButton");
1090 if (doc.id == "app") {
1091 // The head belongs to a screen's tab; the app document has no view and so no head.
1092 for (std::string_view const key : {"subtitle", "status", "actions"}) {
1093 if (member(*object, key) != nullptr) {
1094 refuse(pointerAppend("", key),
1095 "`" + std::string{key} + "` is a screen's; the app document has none");
1096 }
1097 }
1098 }
1099 doc.showErrors = showErrors(*object, "");
1100 if (Json const* const params = member(*object, "params")) {
1101 for (auto const& [name, type] : objectOf(*params, "/params", "`params` maps names to types")) {
1102 std::string const path = pointerAppend("/params", name);
1103 checkName(name, path);
1104 doc.params.emplace_back(name, typeOf(type, path));
1105 }
1106 }
1107 doc.scope = scope(*object, "");
1108 for (std::string_view const key : {"onMount", "onUnmount", "onClose", "onSave", "onBackendChange"}) {
1109 if (Json const* const commands = member(*object, key)) {
1110 doc.lifecycle.emplace(std::string{key}, commandList(*commands, pointerAppend("", key)));
1111 }
1112 }
1113 if (Json const* const keys = member(*object, "keys")) {
1114 doc.keys = keyBindings(*keys, "/keys");
1115 }
1116 if (Json const* const prefetch = member(*object, "prefetch")) {
1117 doc.prefetch = nameList(*prefetch, "/prefetch");
1118 }
1119 if (Json const* const components = member(*object, "components")) {
1120 for (auto const& [name, entry] : objectOf(*components, "/components", "`components` is an object")) {
1121 doc.components.emplace(name, component(name, entry, pointerAppend("/components", name)));
1122 }
1123 }
1124 if (Json const* const points = member(*object, "customize")) {
1125 for (auto const& [name, entry] : objectOf(*points, "/customize", "`customize` is an object")) {
1126 doc.customize.emplace(name, point(name, entry, pointerAppend("/customize", name), doc.id == "app"));
1127 }
1128 }
1129 if (Json const* const view = member(*object, "view")) {
1130 doc.view = node(*view, "/view");
1131 } else if (doc.id != "app") {
1132 refuse("", "a screen has a `view`");
1133 }
1134 return doc;
1135 }
1136
1137private:
1138 [[nodiscard]] static Json const* member(Json::object_t const& object, std::string_view key) {
1139 auto const* const found = object.find(key);
1140 return found == object.end() ? nullptr : &found->second;
1141 }
1142
1143 [[nodiscard]] static Json::object_t const& objectOf(Json const& json, std::string const& path, char const* what) {
1144 auto const* const object = std::get_if<Json::object_t>(&json.data);
1145 if (object == nullptr) {
1146 refuse(path, what);
1147 }
1148 return *object;
1149 }
1150
1151 static void checkName(std::string const& name, std::string const& path) {
1152 if (!isName(name)) {
1153 refuse(path, "`" + name + "` is not a name: names match [A-Za-z_][A-Za-z0-9_]*");
1154 }
1155 }
1156
1157 [[nodiscard]] static std::string text(Json::object_t const& object, std::string_view key, std::string const& path,
1158 bool required) {
1159 Json const* const value = member(object, key);
1160 std::string const keyPath = pointerAppend(path, key);
1161 if (value == nullptr) {
1162 if (required) {
1163 refuse(path, "`" + std::string{key} + "` is required");
1164 }
1165 return {};
1166 }
1167 auto const* const string = std::get_if<std::string>(&value->data);
1168 if (string == nullptr) {
1169 refuse(keyPath, "`" + std::string{key} + "` is a string");
1170 }
1171 return *string;
1172 }
1173
1174 [[nodiscard]] static std::string choiceOf(Json::object_t const& object, std::string_view key,
1175 std::string const& path, std::span<std::string_view const> choices) {
1176 std::string value = text(object, key, path, false);
1177 if (!value.empty() && !listed(choices, value)) {
1178 refuse(pointerAppend(path, key), "`" + value + "` is not one of the values of `" + std::string{key} + "`");
1179 }
1180 return value;
1181 }
1182
1183 [[nodiscard]] static std::string showErrors(Json::object_t const& object, std::string const& path) {
1184 static constexpr std::array<std::string_view, 3> kPolicies = {"touched", "submit", "always"};
1185 return choiceOf(object, "showErrors", path, kPolicies);
1186 }
1187
1188 [[nodiscard]] static std::int64_t integer(Json::object_t const& object, std::string_view key,
1189 std::string const& path) {
1190 Json const* const value = member(object, key);
1191 if (value == nullptr) {
1192 return 0;
1193 }
1194 auto const number = detail::jsonInt(*value);
1195 if (!number || *number < 0) {
1196 refuse(pointerAppend(path, key), "`" + std::string{key} + "` is a non-negative integer");
1197 }
1198 return *number;
1199 }
1200
1201 [[nodiscard]] static bool flag(Json::object_t const& object, std::string_view key, std::string const& path) {
1202 Json const* const value = member(object, key);
1203 if (value == nullptr) {
1204 return false;
1205 }
1206 bool const* const set = std::get_if<bool>(&value->data);
1207 if (set == nullptr) {
1208 refuse(pointerAppend(path, key), "`" + std::string{key} + "` is a bool");
1209 }
1210 return *set;
1211 }
1212
1213 [[nodiscard]] static Type typeOf(Json const& json, std::string const& path) {
1214 auto const* const spelled = std::get_if<std::string>(&json.data);
1215 if (spelled == nullptr) {
1216 refuse(path, "a type is written as a string, such as `int?`");
1217 }
1218 auto type = parseType(*spelled);
1219 if (!type) {
1220 refuse(path, type.error());
1221 }
1222 return *type;
1223 }
1224
1225 [[nodiscard]] static std::vector<std::string> nameList(Json const& json, std::string const& path) {
1226 auto const* const array = std::get_if<Json::array_t>(&json.data);
1227 if (array == nullptr) {
1228 refuse(path, "a list of names is an array of strings");
1229 }
1230 std::vector<std::string> names;
1231 for (std::size_t index = 0; index < array->size(); ++index) {
1232 auto const* const name = std::get_if<std::string>(&array->at(index).data);
1233 if (name == nullptr || name->empty()) {
1234 refuse(pointerAppend(path, index), "a list of names is an array of strings");
1235 }
1236 names.push_back(*name);
1237 }
1238 return names;
1239 }
1240
1241 [[nodiscard]] static ExprPtr expr(Json const& json, std::string const& path) {
1242 auto parsed = parseExpression(json, path);
1243 if (!parsed) {
1244 throw DocumentRefused{std::move(parsed.error())};
1245 }
1246 return std::move(*parsed);
1247 }
1248
1249 [[nodiscard]] static ExprPtr optionalExpr(Json::object_t const& object, std::string_view key,
1250 std::string const& path) {
1251 Json const* const value = member(object, key);
1252 return value == nullptr ? nullptr : expr(*value, pointerAppend(path, key));
1253 }
1254
1255 // A top-level list of parts: the screen head's `status` and `actions`.
1256 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): the document key, then its part kind, at every call.
1257 [[nodiscard]] std::vector<DocNodePtr> topParts(Json::object_t const& object, std::string_view key,
1258 std::string_view kind) {
1259 std::vector<DocNodePtr> parts;
1260 Json const* const value = member(object, key);
1261 if (value == nullptr) {
1262 return parts;
1263 }
1264 std::string const path = pointerAppend("", key);
1265 auto const* const array = std::get_if<Json::array_t>(&value->data);
1266 if (array == nullptr) {
1267 refuse(path, "`" + std::string{key} + "` is an array");
1268 }
1269 for (std::size_t index = 0; index < array->size(); ++index) {
1270 parts.push_back(part(array->at(index), kind, pointerAppend(path, index)));
1271 }
1272 return parts;
1273 }
1274
1275 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one branch per kind of declaration.
1276 [[nodiscard]] ScopeDecl scope(Json::object_t const& object, std::string const& path) {
1277 ScopeDecl decl;
1278 if (Json const* const models = member(object, "models")) {
1279 std::string const where = pointerAppend(path, "models");
1280 for (auto const& [name, entry] : objectOf(*models, where, "`models` maps aliases to models")) {
1281 decl.models.push_back(model(name, entry, pointerAppend(where, name)));
1282 }
1283 }
1284 if (Json const* const state = member(object, "state")) {
1285 std::string const where = pointerAppend(path, "state");
1286 for (auto const& [name, entry] : objectOf(*state, where, "`state` maps names to declarations")) {
1287 std::string const entryPath = pointerAppend(where, name);
1288 checkName(name, entryPath);
1289 auto const& fields = objectOf(entry, entryPath, R"(a state declaration is {"init": e, "type": T})");
1290 refuseMustUnderstand(fields, entryPath);
1291 StateDecl stateDecl{.name = name, .path = entryPath, .type = Type::any(), .init = nullptr};
1292 if (Json const* const type = member(fields, "type")) {
1293 stateDecl.type = typeOf(*type, pointerAppend(entryPath, "type"));
1294 }
1295 stateDecl.init = optionalExpr(fields, "init", entryPath);
1296 decl.state.push_back(std::move(stateDecl));
1297 }
1298 }
1299 if (Json const* const lets = member(object, "let")) {
1300 std::string const where = pointerAppend(path, "let");
1301 for (auto const& [name, entry] : objectOf(*lets, where, "`let` maps names to expressions")) {
1302 std::string const entryPath = pointerAppend(where, name);
1303 checkName(name, entryPath);
1304 decl.lets.push_back(LetDecl{.name = name, .path = entryPath, .value = expr(entry, entryPath)});
1305 }
1306 }
1307 if (Json const* const queries = member(object, "queries")) {
1308 std::string const where = pointerAppend(path, "queries");
1309 for (auto const& [name, entry] : objectOf(*queries, where, "`queries` maps names to queries")) {
1310 decl.queries.push_back(query(name, entry, pointerAppend(where, name)));
1311 }
1312 }
1313 if (Json const* const mutations = member(object, "mutations")) {
1314 std::string const where = pointerAppend(path, "mutations");
1315 for (auto const& [name, entry] : objectOf(*mutations, where, "`mutations` maps names to mutations")) {
1316 decl.mutations.push_back(mutation(name, entry, pointerAppend(where, name)));
1317 }
1318 }
1319 if (Json const* const watches = member(object, "watch")) {
1320 std::string const where = pointerAppend(path, "watch");
1321 auto const* const array = std::get_if<Json::array_t>(&watches->data);
1322 if (array == nullptr) {
1323 refuse(where, "`watch` is an array");
1324 }
1325 for (std::size_t index = 0; index < array->size(); ++index) {
1326 decl.watches.push_back(watch(array->at(index), pointerAppend(where, index)));
1327 }
1328 }
1329 if (Json const* const checks = member(object, "checks")) {
1330 std::string const where = pointerAppend(path, "checks");
1331 auto const* const array = std::get_if<Json::array_t>(&checks->data);
1332 if (array == nullptr) {
1333 refuse(where, "`checks` is an array");
1334 }
1335 for (std::size_t index = 0; index < array->size(); ++index) {
1336 decl.checks.push_back(check(array->at(index), pointerAppend(where, index)));
1337 }
1338 }
1339 return decl;
1340 }
1341
1342 [[nodiscard]] static ModelDecl model(std::string const& name, Json const& json, std::string const& path) {
1343 checkName(name, path);
1344 auto const& fields = objectOf(json, path, R"(a model alias is {"type": ..., "instance": ...})");
1345 refuseMustUnderstand(fields, path);
1346 ModelDecl decl{.name = name,
1347 .path = path,
1348 .type = text(fields, "type", path, true),
1349 .instance = {},
1350 .key = nullptr,
1351 .borrowedFrom = {}};
1352 Json const* const instance = member(fields, "instance");
1353 if (instance == nullptr) {
1354 return decl;
1355 }
1356 std::string const where = pointerAppend(path, "instance");
1357 auto const* const word = std::get_if<std::string>(&instance->data);
1358 if (word != nullptr && (*word == "private" || *word == "shared")) {
1359 decl.instance = *word == "private" ? InstanceMode::Private : InstanceMode::Shared;
1360 return decl;
1361 }
1362 if (auto const* const object = std::get_if<Json::object_t>(&instance->data)) {
1363 if (Json const* const from = member(*object, "from")) {
1364 auto const* const alias = std::get_if<std::string>(&from->data);
1365 if (alias == nullptr || !isName(*alias) || object->size() != 1) {
1366 refuse(where, R"(a borrowed instance is {"from": alias})");
1367 }
1368 decl.instance = InstanceMode::Borrowed;
1369 decl.borrowedFrom = *alias;
1370 return decl;
1371 }
1372 }
1373 decl.instance = InstanceMode::Keyed;
1374 decl.key = expr(*instance, where);
1375 return decl;
1376 }
1377
1378 /// @brief Refuses @p key of @p fields: a refresh key, which a document does not declare.
1379 /// @param fields The declaration's fields.
1380 /// @param path The declaration's JSON Pointer.
1381 /// @param key The refresh key: `invalidates`, `patch` or `refreshOn`.
1382 static void refuseRefreshKey(Json::object_t const& fields, std::string const& path, std::string_view key) {
1383 if (member(fields, key) != nullptr) {
1384 refuse(pointerAppend(path, key),
1385 std::format("a document does not declare `{}`: an action's effects say what it changes, and every "
1386 "query holding a changed record refreshes; a refresh no action declares is a `refetch` "
1387 "command",
1388 key));
1389 }
1390 }
1391
1392 [[nodiscard]] static QueryDecl query(std::string const& name, Json const& json, std::string const& path) {
1393 checkName(name, path);
1394 auto const& fields = objectOf(json, path, "a query is an object");
1395 refuseMustUnderstand(fields, path);
1396 QueryDecl decl;
1397 decl.name = name;
1398 decl.path = path;
1399 decl.model = text(fields, "model", path, true);
1400 std::string const action = text(fields, "action", path, false);
1401 std::string const evaluate = text(fields, "evaluate", path, false);
1402 if (action.empty() == evaluate.empty()) {
1403 refuse(path, "a query names exactly one of `action` and `evaluate`");
1404 }
1405 decl.action = action.empty() ? evaluate : action;
1406 decl.evaluate = !evaluate.empty();
1407 decl.body = optionalExpr(fields, "body", path);
1408 decl.when = optionalExpr(fields, "when", path);
1409 decl.debounce = integer(fields, "debounce", path);
1410 decl.refreshEvery = integer(fields, "refreshEvery", path);
1411 decl.staleTime = integer(fields, "staleTime", path);
1412 refuseRefreshKey(fields, path, "refreshOn");
1413 return decl;
1414 }
1415
1416 /// @brief Parses an `optimistic` write: `{"query": q, "at"?: m, "key": e, "with": e}`.
1417 /// @param json The write's JSON.
1418 /// @param path Its JSON Pointer.
1419 /// @return The write.
1420 [[nodiscard]] static OptimisticDecl optimistic(Json const& json, std::string const& path) {
1421 static constexpr char const* shape = R"(an optimistic write is {"query": q, "at"?: m, "key": e, "with": e})";
1422 auto const& fields = objectOf(json, path, shape);
1423 refuseMustUnderstand(fields, path);
1424 Json const* const key = member(fields, "key");
1425 Json const* const with = member(fields, "with");
1426 if (key == nullptr || with == nullptr) {
1427 refuse(path, shape);
1428 }
1429 std::string listMember = text(fields, "at", path, false);
1430 if (member(fields, "at") != nullptr && listMember.empty()) {
1431 refuse(pointerAppend(path, "at"), "`at` names the member of the query's record that holds the list");
1432 }
1433 return OptimisticDecl{.query = text(fields, "query", path, true),
1434 .at = std::move(listMember),
1435 .key = expr(*key, pointerAppend(path, "key")),
1436 .with = expr(*with, pointerAppend(path, "with"))};
1437 }
1438
1439 [[nodiscard]] MutationDecl mutation(std::string const& name, Json const& json, std::string const& path) {
1440 checkName(name, path);
1441 auto const& fields = objectOf(json, path, "a mutation is an object");
1442 refuseMustUnderstand(fields, path);
1443 MutationDecl decl;
1444 decl.name = name;
1445 decl.path = path;
1446 decl.model = text(fields, "model", path, true);
1447 decl.action = text(fields, "action", path, true);
1448 decl.body = optionalExpr(fields, "body", path);
1449 static constexpr std::array<std::string_view, 4> kModes = {"exclusive", "serial", "latest", "parallel"};
1450 std::string const mode = choiceOf(fields, "concurrency", path, kModes);
1451 if (mode == "serial") {
1452 decl.concurrency = Concurrency::Serial;
1453 } else if (mode == "latest") {
1454 decl.concurrency = Concurrency::Latest;
1455 } else if (mode == "parallel") {
1456 decl.concurrency = Concurrency::Parallel;
1457 }
1458 decl.serialKey = optionalExpr(fields, "serialKey", path);
1459 if (decl.serialKey && decl.concurrency != Concurrency::Serial) {
1460 refuse(pointerAppend(path, "serialKey"), "`serialKey` applies to `serial` concurrency only");
1461 }
1462 refuseRefreshKey(fields, path, "invalidates");
1463 refuseRefreshKey(fields, path, "patch");
1464 if (Json const* const value = member(fields, "optimistic")) {
1465 decl.optimistic = optimistic(*value, pointerAppend(path, "optimistic"));
1466 }
1467 if (Json const* const commands = member(fields, "onSuccess")) {
1468 decl.onSuccess = commandList(*commands, pointerAppend(path, "onSuccess"));
1469 }
1470 if (Json const* const commands = member(fields, "onError")) {
1471 decl.onError = commandList(*commands, pointerAppend(path, "onError"));
1472 }
1473 static constexpr std::array<std::string_view, 1> kOffline = {"queue"};
1474 decl.offlineQueue = !choiceOf(fields, "offline", path, kOffline).empty();
1475 return decl;
1476 }
1477
1478 [[nodiscard]] WatchDecl watch(Json const& json, std::string const& path) {
1479 auto const& fields = objectOf(json, path, R"(a watch is {"on": e, "do": [...]})");
1480 refuseMustUnderstand(fields, path);
1481 Json const* const watched = member(fields, "on");
1482 Json const* const body = member(fields, "do");
1483 if (watched == nullptr || body == nullptr) {
1484 refuse(path, R"(a watch is {"on": e, "do": [...]})");
1485 }
1486 static constexpr std::array<std::string_view, 2> kSources = {"any", "user"};
1487 return WatchDecl{
1488 .path = path,
1489 .on = expr(*watched, pointerAppend(path, "on")),
1490 .source = choiceOf(fields, "source", path, kSources) == "user" ? WatchSource::User : WatchSource::Any,
1491 .body = commandList(*body, pointerAppend(path, "do"))};
1492 }
1493
1494 [[nodiscard]] static CheckDecl check(Json const& json, std::string const& path) {
1495 auto const& fields = objectOf(json, path, R"(a check is {"id": ..., "assert": e, ...})");
1496 refuseMustUnderstand(fields, path);
1497 Json const* const assertion = member(fields, "assert");
1498 if (assertion == nullptr) {
1499 refuse(path, "a check has an `assert`");
1500 }
1501 CheckDecl decl;
1502 decl.id = text(fields, "id", path, false);
1503 decl.path = path;
1504 decl.assertion = expr(*assertion, pointerAppend(path, "assert"));
1505 decl.message = optionalExpr(fields, "message", path);
1506 if (Json const* const targets = member(fields, "targets")) {
1507 decl.targets = nameList(*targets, pointerAppend(path, "targets"));
1508 }
1509 decl.blocking = flag(fields, "blocking", path);
1510 return decl;
1511 }
1512
1513 struct CommandSpec {
1514 std::string_view key;
1515 CommandKind kind;
1516 std::array<std::string_view, 6> companions;
1517 };
1518
1519 // NOLINTBEGIN(modernize-use-designated-initializers): one row per command reads as a table.
1520 static constexpr std::array<CommandSpec, 23> kCommands = {{
1521 {"set", CommandKind::Set, {"value"}},
1522 {"reset", CommandKind::Reset, {}},
1523 {"run", CommandKind::Run, {"body", "as", "each"}},
1524 {"refetch", CommandKind::Refetch, {}},
1525 {"open", CommandKind::Open, {}},
1526 {"close", CommandKind::Close, {}},
1527 {"navigate", CommandKind::Navigate, {"params", "as", "onResult", "onCancel"}},
1528 {"return", CommandKind::Return, {}},
1529 {"focus", CommandKind::Focus, {}},
1530 {"host", CommandKind::Host, {"args"}},
1531 {"switchBackend", CommandKind::SwitchBackend, {}},
1532 {"resetPrefs", CommandKind::ResetPrefs, {}},
1533 {"setLocale", CommandKind::SetLocale, {}},
1534 {"notify", CommandKind::Notify, {"role"}},
1535 {"seq", CommandKind::Seq, {}},
1536 {"all", CommandKind::All, {}},
1537 {"if", CommandKind::If, {}},
1538 {"cancelClose", CommandKind::CancelClose, {}},
1539 {"retry", CommandKind::Retry, {}},
1540 {"signIn", CommandKind::SignIn, {}},
1541 {"signOut", CommandKind::SignOut, {}},
1542 {"upload", CommandKind::Upload, {"to", "as", "id", "into", "nameAs", "typeAs"}},
1543 {"download", CommandKind::Download, {"save", "name", "to", "as", "from", "type"}},
1544 }};
1545 // NOLINTEND(modernize-use-designated-initializers)
1546
1547 [[nodiscard]] Commands commandList(Json const& json, std::string const& path) {
1548 Commands commands;
1549 if (auto const* const array = std::get_if<Json::array_t>(&json.data)) {
1550 for (std::size_t index = 0; index < array->size(); ++index) {
1551 commands.push_back(command(array->at(index), pointerAppend(path, index)));
1552 }
1553 return commands;
1554 }
1555 commands.push_back(command(json, path));
1556 return commands;
1557 }
1558
1559 [[nodiscard]] static std::string nameOf(Json const& json, std::string const& path, char const* what,
1560 bool dotted = false) {
1561 auto const* const name = std::get_if<std::string>(&json.data);
1562 bool valid = name != nullptr && !name->empty();
1563 if (valid && dotted) {
1564 auto const segments = splitPath(*name);
1565 valid = segments.has_value() && isName(segments->front());
1566 } else if (valid) {
1567 valid = isName(*name);
1568 }
1569 if (!valid) {
1570 refuse(path, what);
1571 }
1572 return *name;
1573 }
1574
1575 [[nodiscard]] static std::string nonEmpty(Json const& json, std::string const& path, char const* what) {
1576 auto const* const name = std::get_if<std::string>(&json.data);
1577 if (name == nullptr || name->empty()) {
1578 refuse(path, what);
1579 }
1580 return *name;
1581 }
1582
1583 static void requireTrue(Json const& json, std::string const& path, std::string_view key) {
1584 bool const* const value = std::get_if<bool>(&json.data);
1585 if (value == nullptr || !*value) {
1586 refuse(path, "`" + std::string{key} + "` is written `true`");
1587 }
1588 }
1589
1590 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one case per command of ui/1.
1591 [[nodiscard]] Command command(Json const& json, std::string const& path) {
1592 auto const& fields = objectOf(json, path, R"(a command is an object such as {"set": ..., "value": ...})");
1593 CommandSpec const* chosen = nullptr;
1594 for (auto const& [key, value] : fields) {
1595 static_cast<void>(value);
1596 for (CommandSpec const& spec : kCommands) {
1597 if (spec.key == key) {
1598 if (chosen != nullptr) {
1599 refuse(path,
1600 "a command has one kind; found `" + std::string{chosen->key} + "` and `" + key + "`");
1601 }
1602 chosen = &spec;
1603 }
1604 }
1605 }
1606 if (chosen == nullptr) {
1607 refuse(path, "not a command of ui/1");
1608 }
1609 refuseMustUnderstand(fields, path);
1610 Command cmd;
1611 cmd.kind = chosen->kind;
1612 cmd.path = path;
1613 Json const& primary = *member(fields, chosen->key);
1614 std::string const where = pointerAppend(path, chosen->key);
1615 switch (chosen->kind) {
1616 case CommandKind::Set: {
1617 cmd.target = nameOf(primary, where, "`set` names state or an element as `name.path`", true);
1618 Json const* const value = member(fields, "value");
1619 if (value == nullptr) {
1620 refuse(path, "`set` needs a `value`");
1621 }
1622 cmd.value = expr(*value, pointerAppend(path, "value"));
1623 break;
1624 }
1625 case CommandKind::Run:
1626 cmd.target = nameOf(primary, where, "`run` names a mutation");
1627 cmd.value = optionalExpr(fields, "body", path);
1628 cmd.extra = optionalExpr(fields, "each", path);
1629 if (Json const* const binding = member(fields, "as")) {
1630 cmd.binding = nameOf(*binding, pointerAppend(path, "as"), "`as` is a name");
1631 }
1632 break;
1633 case CommandKind::Reset:
1634 case CommandKind::Refetch:
1635 case CommandKind::Open:
1636 case CommandKind::Focus:
1637 cmd.target = nameOf(primary, where, "the command names a declaration or an element");
1638 break;
1639 case CommandKind::Close:
1640 cmd.target = nameOf(primary, where, "`close` names a dialog, or `self`");
1641 break;
1642 case CommandKind::Navigate:
1643 navigate(cmd, fields, primary, path);
1644 break;
1645 case CommandKind::Return:
1646 cmd.value = expr(primary, where);
1647 break;
1648 case CommandKind::Host:
1649 cmd.target = nonEmpty(primary, where, "`host` names a registered command");
1650 cmd.value = optionalExpr(fields, "args", path);
1651 break;
1652 case CommandKind::SwitchBackend:
1653 case CommandKind::SetLocale:
1654 cmd.target = nonEmpty(primary, where, "the command takes a non-empty string");
1655 break;
1656 case CommandKind::ResetPrefs:
1657 cmd.target = nameOf(primary, where, "`resetPrefs` names a customization point, or `all`");
1658 break;
1659 case CommandKind::Notify: {
1660 cmd.value = expr(primary, where);
1661 static constexpr std::array<std::string_view, 4> kRoles = {"info", "success", "warning", "error"};
1662 cmd.option = choiceOf(fields, "role", path, kRoles);
1663 break;
1664 }
1665 case CommandKind::Seq:
1666 case CommandKind::All: {
1667 if (!std::holds_alternative<Json::array_t>(primary.data)) {
1668 refuse(where, "`" + std::string{chosen->key} + "` takes an array of commands");
1669 }
1670 cmd.commands = commandList(primary, where);
1671 break;
1672 }
1673 case CommandKind::If: {
1674 auto const* const array = std::get_if<Json::array_t>(&primary.data);
1675 if (array == nullptr || array->size() < 2 || array->size() > 3) {
1676 refuse(where, "`if` takes [condition, [then...], [else...]]");
1677 }
1678 cmd.value = expr(array->at(0), pointerAppend(where, std::size_t{0}));
1679 cmd.commands = commandList(array->at(1), pointerAppend(where, 1));
1680 if (array->size() == 3) {
1681 cmd.alternative = commandList(array->at(2), pointerAppend(where, 2));
1682 }
1683 break;
1684 }
1685 case CommandKind::CancelClose:
1686 case CommandKind::Retry:
1687 case CommandKind::SignOut:
1688 requireTrue(primary, where, chosen->key);
1689 break;
1690 case CommandKind::SignIn: {
1691 auto const* const object = std::get_if<Json::object_t>(&primary.data);
1692 Json const* const from = object == nullptr ? nullptr : member(*object, "from");
1693 if (from == nullptr) {
1694 refuse(where, R"(`signIn` is {"from": mutation})");
1695 }
1696 cmd.target = nameOf(*from, pointerAppend(where, "from"), "`signIn` names its mutation");
1697 break;
1698 }
1699 case CommandKind::Upload: {
1700 cmd.value = expr(primary, where);
1701 Json const* const destination = member(fields, "to");
1702 if (destination == nullptr) {
1703 refuse(path, "`upload` names its mutation with `to`");
1704 }
1705 cmd.target = nameOf(*destination, pointerAppend(path, "to"), "`to` names a mutation");
1706 if (Json const* const binding = member(fields, "as")) {
1707 cmd.binding = nameOf(*binding, pointerAppend(path, "as"), "`as` is a name");
1708 }
1709 if (Json const* const transfer = member(fields, "id")) {
1710 cmd.transfer = nameOf(*transfer, pointerAppend(path, "id"), "`id` is a name");
1711 }
1712 static constexpr std::array<std::string_view, 2> kInto = {"ref", "bytes"};
1713 cmd.bytes = choiceOf(fields, "into", path, kInto) == "bytes";
1714 for (auto const& [key, slot] : {std::pair{"nameAs", &cmd.nameAs}, std::pair{"typeAs", &cmd.typeAs}}) {
1715 if (Json const* const name = member(fields, key)) {
1716 if (!cmd.bytes) {
1717 refuse(pointerAppend(path, key),
1718 "`" + std::string{key} +
1719 R"(` names a body member for inline bytes; give `"into": "bytes"`)");
1720 }
1721 *slot = nameOf(*name, pointerAppend(path, key), "a body member is a name");
1722 }
1723 }
1724 break;
1725 }
1726 case CommandKind::Download:
1727 default:
1728 download(cmd, fields, primary, path);
1729 break;
1730 }
1731 return cmd;
1732 }
1733
1734 void navigate(Command& cmd, Json::object_t const& fields, Json const& primary, std::string const& path) {
1735 std::string const where = pointerAppend(path, "navigate");
1736 if (auto const* const route = std::get_if<Json::object_t>(&primary.data)) {
1737 Json const* const value = member(*route, "route");
1738 if (value == nullptr || route->size() != 1) {
1739 refuse(where, R"(`navigate` names a screen, or is {"route": e})");
1740 }
1741 cmd.extra = expr(*value, pointerAppend(where, "route"));
1742 } else {
1743 cmd.target = nonEmpty(primary, where, R"(`navigate` names a screen, or is {"route": e})");
1744 }
1745 cmd.value = optionalExpr(fields, "params", path);
1746 static constexpr std::array<std::string_view, 4> kModes = {"tab", "dialog", "drawer", "window"};
1747 cmd.option = choiceOf(fields, "as", path, kModes);
1748 if (Json const* const result = member(fields, "onResult")) {
1749 cmd.commands = commandList(*result, pointerAppend(path, "onResult"));
1750 }
1751 if (Json const* const cancel = member(fields, "onCancel")) {
1752 cmd.alternative = commandList(*cancel, pointerAppend(path, "onCancel"));
1753 }
1754 }
1755
1756 static void download(Command& cmd, Json::object_t const& fields, Json const& primary, std::string const& path) {
1757 cmd.value = expr(primary, pointerAppend(path, "download"));
1758 static constexpr std::array<std::string_view, 2> kFrom = {"ref", "bytes"};
1759 cmd.bytes = choiceOf(fields, "from", path, kFrom) == "bytes";
1760 if (cmd.bytes) {
1761 // Inline bytes carry no name or type of their own, so the document gives the name.
1762 if (member(fields, "name") == nullptr) {
1763 refuse(path, "a download from bytes names its file with `name`");
1764 }
1765 cmd.extra = optionalExpr(fields, "name", path);
1766 cmd.mediaType = optionalExpr(fields, "type", path);
1767 } else if (member(fields, "type") != nullptr) {
1768 refuse(pointerAppend(path, "type"), R"(`type` is the media type of inline bytes; give `"from": "bytes"`)");
1769 }
1770 std::string const save = text(fields, "save", path, false);
1771 std::string const destination = text(fields, "to", path, false);
1772 if (save == "dialog" && destination.empty()) {
1773 cmd.option = "dialog";
1774 cmd.extra = optionalExpr(fields, "name", path);
1775 return;
1776 }
1777 if (destination == "preview" && save.empty()) {
1778 Json const* const binding = member(fields, "as");
1779 if (binding == nullptr) {
1780 refuse(path, "a download to preview binds a state with `as`");
1781 }
1782 cmd.option = "preview";
1783 cmd.target = nameOf(*binding, pointerAppend(path, "as"), "`as` names a state");
1784 return;
1785 }
1786 refuse(path, R"(`download` takes `"save": "dialog"` or `"to": "preview"`)");
1787 }
1788
1789 [[nodiscard]] std::map<std::string, Commands, std::less<>> keyBindings(Json const& json, std::string const& path) {
1790 std::map<std::string, Commands, std::less<>> keys;
1791 for (auto const& [chord, commands] : objectOf(json, path, "`keys` maps chords to commands")) {
1792 std::string const where = pointerAppend(path, chord);
1793 checkChord(chord, where);
1794 keys.emplace(chord, commandList(commands, where));
1795 }
1796 return keys;
1797 }
1798
1799 // NOLINTNEXTLINE(readability-function-cognitive-complexity): a component's rules, one after another.
1800 [[nodiscard]] Component component(std::string const& name, Json const& json, std::string const& path) {
1801 checkName(name, path);
1802 auto const& fields = objectOf(json, path, "a component is an object");
1803 refuseMustUnderstand(fields, path);
1804 Component decl;
1805 decl.name = name;
1806 decl.path = path;
1807 decl.qml = text(fields, "qml", path, false);
1808 decl.host = text(fields, "host", path, false);
1809 if (decl.qml.empty() == decl.host.empty()) {
1810 refuse(path, "a component names exactly one of `qml` and `host`");
1811 }
1812 if (!decl.qml.empty()) {
1813 bool hash = decl.qml.size() == 71 && decl.qml.starts_with("sha256:");
1814 for (char const chr : std::string_view{decl.qml}.substr(std::min<std::size_t>(7, decl.qml.size()))) {
1815 hash = hash && ((chr >= '0' && chr <= '9') || (chr >= 'a' && chr <= 'f'));
1816 }
1817 if (!hash) {
1818 refuse(pointerAppend(path, "qml"), "`qml` is a content hash `sha256:<64 hex digits>`");
1819 }
1820 } else {
1821 Json const* const version = member(fields, "version");
1822 auto const number = version == nullptr ? std::nullopt : detail::jsonInt(*version);
1823 if (!number) {
1824 refuse(path, "a host component has an integer `version`");
1825 }
1826 decl.version = *number;
1827 }
1828 static constexpr std::array<std::string_view, 6> kPropTypes = {"string", "bool", "int",
1829 "exact", "number", "json"};
1830 if (Json const* const props = member(fields, "props")) {
1831 std::string const where = pointerAppend(path, "props");
1832 for (auto const& [prop, type] : objectOf(*props, where, "`props` maps names to prop types")) {
1833 auto const* const spelled = std::get_if<std::string>(&type.data);
1834 if (spelled == nullptr || !listed(kPropTypes, *spelled)) {
1835 refuse(pointerAppend(where, prop), "a prop type is string, bool, int, exact, number or json");
1836 }
1837 decl.props.emplace(prop, *spelled);
1838 }
1839 }
1840 if (Json const* const events = member(fields, "events")) {
1841 std::string const where = pointerAppend(path, "events");
1842 for (auto const& [event, type] : objectOf(*events, where, "`events` maps names to payload types")) {
1843 decl.events.emplace(event, typeOf(type, pointerAppend(where, event)));
1844 }
1845 }
1846 Json const* const fallback = member(fields, "fallback");
1847 if (fallback == nullptr) {
1848 refuse(path, "a component names a `fallback` of built-in nodes");
1849 }
1850 decl.fallback = node(*fallback, pointerAppend(path, "fallback"));
1851 return decl;
1852 }
1853
1854 [[nodiscard]] static CustomizePoint point(std::string const& name, Json const& json, std::string const& path,
1855 bool appDocument) {
1856 checkName(name, path);
1857 auto const& fields = objectOf(json, path, "a customization point is an object");
1858 refuseMustUnderstand(fields, path);
1859 static constexpr std::array<std::string_view, 7> kPointKinds = {"columns", "choice", "value", "layout",
1860 "saved", "pins", "arrange"};
1861 CustomizePoint decl;
1862 decl.name = name;
1863 decl.path = path;
1864 decl.kind = text(fields, "kind", path, true);
1865 if (!listed(kPointKinds, decl.kind)) {
1866 refuse(pointerAppend(path, "kind"), "`" + decl.kind + "` is not a kind of customization point");
1867 }
1868 if (decl.kind == "pins" && !appDocument) {
1869 refuse(pointerAppend(path, "kind"), "a `pins` point belongs to the app document");
1870 }
1871 // The container an `arrange` point orders is a screen's, and the app document has no view.
1872 if (decl.kind == "arrange" && appDocument) {
1873 refuse(pointerAppend(path, "kind"), "an `arrange` point belongs to the screen whose container it orders");
1874 }
1875 decl.of = text(fields, "of", path, decl.kind == "columns" || decl.kind == "arrange");
1876 Json const* const fallback = member(fields, "default");
1877 if (decl.kind == "choice") {
1878 Json const* const options = member(fields, "options");
1879 if (options == nullptr) {
1880 refuse(path, "a `choice` point lists its `options`");
1881 }
1882 decl.options = nameList(*options, pointerAppend(path, "options"));
1883 if (fallback != nullptr) {
1884 auto const* const chosen = std::get_if<std::string>(&fallback->data);
1885 if (chosen == nullptr || std::ranges::find(decl.options, *chosen) == decl.options.end()) {
1886 refuse(pointerAppend(path, "default"), "the default of a `choice` is one of its options");
1887 }
1888 decl.defaultValue = *chosen;
1889 }
1890 decl.type = Type::of(TypeKind::String);
1891 } else if (decl.kind == "value") {
1892 Json const* const type = member(fields, "type");
1893 if (type == nullptr) {
1894 refuse(path, "a `value` point declares its `type`");
1895 }
1896 decl.type = typeOf(*type, pointerAppend(path, "type"));
1897 if (fallback != nullptr) {
1898 auto value = valueFromJson(*fallback, decl.type);
1899 if (!value) {
1900 refuse(pointerAppend(path, "default"),
1901 "the default does not have the point's type: " + value.error());
1902 }
1903 decl.defaultValue = std::move(*value);
1904 }
1905 }
1906 return decl;
1907 }
1908
1909 [[nodiscard]] DocNodePtr node(Json const& json, std::string const& path) {
1910 auto const* const object = std::get_if<Json::object_t>(&json.data);
1911 if (object == nullptr) {
1912 refuse(path, "a node is an object with a `kind`");
1913 }
1914 std::string const kind = text(*object, "kind", path, true);
1915 KindSpec const* const spec = findKind(kind, false);
1916 if (spec == nullptr) {
1917 refuse(pointerAppend(path, "kind"), "the node kind `" + kind + "` is not in ui/1");
1918 }
1919 auto made = std::make_shared<DocNode>();
1920 made->kind = kind;
1921 made->path = path;
1922 refuseMustUnderstand(*object, path);
1923 fill(*made, *object, *spec, path);
1924 return made;
1925 }
1926
1927 void fill(DocNode& made, Json::object_t const& object, KindSpec const& spec, std::string const& path) {
1928 bool const isPart = (spec.flags & kPart) != 0;
1929 auto apply = [&](KeySpec const& key) {
1930 Json const* const value = member(object, key.key);
1931 if (value == nullptr) {
1932 if ((key.flags & kRequired) != 0) {
1933 refuse(path, "`" + std::string{key.key} + "` is required on `" + std::string{spec.kind} + "`");
1934 }
1935 return;
1936 }
1937 entry(made, key, *value, pointerAppend(path, key.key));
1938 };
1939 if (!isPart) {
1940 for (KeySpec const& key : kCommonKeys) {
1941 apply(key);
1942 }
1943 if ((spec.flags & kField) != 0) {
1944 for (KeySpec const& key : kFieldKeys) {
1945 apply(key);
1946 }
1947 }
1948 }
1949 for (KeySpec const& key : spec.keys) {
1950 apply(key);
1951 }
1952 if ((spec.flags & kScope) != 0) {
1953 if (Json const* const declarations = member(object, "scope")) {
1954 std::string const where = pointerAppend(path, "scope");
1955 auto const& fields = objectOf(*declarations, where, "`scope` holds a scope's declarations");
1956 refuseMustUnderstand(fields, where);
1957 made.scope = scope(fields, where);
1958 } else {
1959 made.scope = ScopeDecl{};
1960 }
1961 }
1962 if (made.kind == "fields") {
1963 for (auto const& [field, overlay] : object) {
1964 made.parts["fields"].push_back(fieldOverlay(field, overlay, pointerAppend(path, field)));
1965 }
1966 }
1967 }
1968
1969 // One field overlay of a form's `fields`: expressions by key, and the host `component` that draws the field with
1970 // its `props`.
1971 [[nodiscard]] static DocNodePtr fieldOverlay(std::string const& field, Json const& overlay,
1972 std::string const& where) {
1973 auto const& settings = objectOf(overlay, where, "a field overlay is an object");
1974 auto part = std::make_shared<DocNode>();
1975 part->kind = "field";
1976 part->id = field;
1977 part->path = where;
1978 for (auto const& [key, value] : settings) {
1979 std::string const keyPath = pointerAppend(where, key);
1980 if (key == "component") {
1981 // The host component that draws the field, and the props it is given beside the field's.
1982 auto const* const name = std::get_if<std::string>(&value.data);
1983 if (name == nullptr || !isName(*name)) {
1984 refuse(keyPath, "`component` is a name");
1985 }
1986 part->names.emplace(key, *name);
1987 } else if (key == "props") {
1988 auto record = std::make_shared<DocNode>();
1989 record->kind = "props";
1990 record->path = keyPath;
1991 for (auto const& [prop, propValue] : objectOf(value, keyPath, "`props` is a record of expressions")) {
1992 record->props.emplace(prop, expr(propValue, pointerAppend(keyPath, prop)));
1993 }
1994 part->parts["props"].push_back(std::move(record));
1995 } else {
1996 part->props.emplace(key, expr(value, keyPath));
1997 }
1998 }
1999 return part;
2000 }
2001
2002 [[nodiscard]] DocNodePtr part(Json const& json, std::string_view kind, std::string const& path) {
2003 KindSpec const* const spec = findKind(kind, true);
2004 auto const& object = objectOf(json, path, "a part of a node is an object");
2005 refuseMustUnderstand(object, path);
2006 auto made = std::make_shared<DocNode>();
2007 made->kind = std::string{kind};
2008 made->path = path;
2009 fill(*made, object, *spec, path);
2010 return made;
2011 }
2012
2013 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one case per shape a key takes.
2014 void entry(DocNode& made, KeySpec const& key, Json const& value, std::string const& path) {
2015 std::string const name{key.key};
2016 switch (key.shape) {
2017 case Shape::Expr:
2018 case Shape::Condition:
2019 made.props.emplace(name, expr(value, path));
2020 break;
2021 case Shape::Commands:
2022 made.events.emplace(name, commandList(value, path));
2023 break;
2024 case Shape::Nodes: {
2025 std::vector<DocNodePtr> nodes;
2026 if (auto const* const array = std::get_if<Json::array_t>(&value.data)) {
2027 for (std::size_t index = 0; index < array->size(); ++index) {
2028 nodes.push_back(node(array->at(index), pointerAppend(path, index)));
2029 }
2030 } else {
2031 nodes.push_back(node(value, path));
2032 }
2033 made.children.emplace(name, std::move(nodes));
2034 break;
2035 }
2036 case Shape::Text:
2037 case Shape::Name: {
2038 auto const* const string = std::get_if<std::string>(&value.data);
2039 if (string == nullptr || (key.shape == Shape::Name && !isName(*string))) {
2040 refuse(path, "`" + name + "` is " + (key.shape == Shape::Name ? "a name" : "a string"));
2041 }
2042 if (name == "id") {
2043 made.id = *string;
2044 } else {
2045 made.names.emplace(name, *string);
2046 }
2047 break;
2048 }
2049 case Shape::Part:
2050 made.parts[name].push_back(part(value, key.part, path));
2051 break;
2052 case Shape::Parts:
2053 case Shape::PartsOrExpr: {
2054 auto const* const array = std::get_if<Json::array_t>(&value.data);
2055 if (array == nullptr && key.shape == Shape::PartsOrExpr) {
2056 made.props.emplace(name, expr(value, path));
2057 break;
2058 }
2059 if (array == nullptr) {
2060 refuse(path, "`" + name + "` is an array");
2061 }
2062 auto& list = made.parts[name];
2063 for (std::size_t index = 0; index < array->size(); ++index) {
2064 list.push_back(part(array->at(index), key.part, pointerAppend(path, index)));
2065 }
2066 break;
2067 }
2068 case Shape::ExprRecord: {
2069 if (!key.part.empty()) {
2070 made.parts[name].push_back(part(value, key.part, path));
2071 break;
2072 }
2073 auto record = std::make_shared<DocNode>();
2074 record->kind = name;
2075 record->path = path;
2076 for (auto const& [field, fieldValue] : objectOf(value, path, "a record of expressions")) {
2077 record->props.emplace(field, expr(fieldValue, pointerAppend(path, field)));
2078 }
2079 made.parts[name].push_back(std::move(record));
2080 break;
2081 }
2082 case Shape::CommandRecord:
2083 for (auto const& [event, commands] : objectOf(value, path, "a record of command lists")) {
2084 made.events.emplace(std::format("{}.{}", name, event),
2085 commandList(commands, pointerAppend(path, event)));
2086 }
2087 break;
2088 case Shape::Flag: {
2089 auto const* const flag = std::get_if<bool>(&value.data);
2090 if (flag == nullptr) {
2091 refuse(path, "`" + name + "` is a literal `true` or `false`");
2092 }
2093 made.flags.emplace(name, *flag);
2094 break;
2095 }
2096 case Shape::Keys:
2097 default:
2098 made.keys = keyBindings(value, path);
2099 break;
2100 }
2101 }
2102 // NOLINTEND(misc-no-recursion)
2103};
2104
2105/// @brief What a name in a scope declares.
2106enum class NameCategory : std::uint8_t {
2107 Param, ///< A screen param.
2108 Model, ///< A model alias.
2109 State, ///< State.
2110 Let, ///< A derived value.
2111 Query, ///< A query.
2112 Mutation, ///< A mutation.
2113 Element, ///< An element id.
2114 Transfer, ///< A transfer id of `upload`.
2115};
2116
2117struct ScopeInfo;
2118
2119/// @brief One declared name of a scope.
2121 /// @brief What it declares.
2123 /// @brief The declaration's JSON Pointer.
2124 std::string path;
2125 /// @brief The scope that declares it.
2126 ScopeInfo* scope = nullptr;
2127 /// @brief The type of a param, of typed state, and of state or a `let` once derived.
2129 /// @brief The model of a `Model` entry.
2130 ModelDecl const* model = nullptr;
2131 /// @brief The expression a state or `let` type is derived from; `nullptr` when the type is declared.
2133 /// @brief The query of a `Query` entry.
2134 QueryDecl const* query = nullptr;
2135 /// @brief The mutation of a `Mutation` entry.
2136 MutationDecl const* mutation = nullptr;
2137 /// @brief The element of an `Element` entry.
2138 DocNode const* element = nullptr;
2139 /// @brief 0 before its type is derived, 1 while it is, 2 after.
2140 int visit = 0;
2141};
2142
2143/// @brief One scope during validation: its names and its parent.
2145 /// @brief The enclosing scope; `nullptr` for a screen or a component fallback.
2146 ScopeInfo* parent = nullptr;
2147 /// @brief Whether it is a `forEach` or `table` row scope.
2148 bool row = false;
2149 /// @brief Whether it is the screen's own scope.
2150 bool screen = false;
2151 /// @brief The names it declares.
2152 std::map<std::string, NameEntry, std::less<>> names;
2153};
2154
2155/// @brief Where an expression or command is checked: its scope and what is valid there.
2157 /// @brief The scope.
2158 ScopeInfo* scope = nullptr;
2159 /// @brief Inside a command list, where `event` is valid.
2160 bool commands = false;
2161 /// @brief Inside a boundary's `error`, where `boundary.error` and `retry` are valid.
2162 bool boundaryError = false;
2163 /// @brief Inside `onClose`, where `cancelClose` is valid.
2164 bool onClose = false;
2165 /// @brief Names bound here: `as` of a sequence step or `each`, `result`, `body`, `props`.
2166 std::vector<std::pair<std::string, Type>> bindings;
2167};
2168
2169class DocumentValidator;
2170
2171/// @brief Resolves an expression's names against a scope chain and a context.
2172class ScopeResolver final : public NameResolver {
2173public:
2174 /// @brief Resolves for @p validator in @p context.
2175 /// @param validator The validator, which derives state and `let` types on demand.
2176 /// @param context Where the expression is.
2178 : _validator{validator}, _context{context} {}
2179
2180 /// @brief Resolves a reference.
2181 /// @param head The first segment.
2182 /// @param path The rest.
2183 /// @return The type, or why it does not resolve.
2184 [[nodiscard]] std::expected<Type, std::string> ref(std::string_view head,
2185 std::span<std::string const> path) const override;
2186
2187 /// @brief Resolves a row field.
2188 /// @param path The field path.
2189 /// @return `any` inside a row, or why there is no row.
2190 [[nodiscard]] std::expected<Type, std::string> row(std::span<std::string const> path) const override;
2191
2192 /// @brief Resolves a query or mutation name.
2193 /// @param form The form.
2194 /// @param name The name.
2195 /// @return The type, or why it does not resolve.
2196 [[nodiscard]] std::expected<Type, std::string> control(ExprKind form, std::string_view name) const override;
2197
2198 /// @brief Resolves a customization point.
2199 /// @param point The point.
2200 /// @param path The rest.
2201 /// @return The type, or why it does not resolve.
2202 [[nodiscard]] std::expected<Type, std::string> pref(std::string_view point,
2203 std::span<std::string const> path) const override;
2204
2205private:
2206 DocumentValidator& _validator;
2207 CheckContext const& _context;
2208};
2209
2210/// @brief The type of an element value.
2211/// @param value The value's name.
2212/// @return Its type.
2213[[nodiscard]] inline Type elementValueType(std::string_view value) {
2214 static constexpr std::array<std::string_view, 10> kBools = {"visible", "enabled", "valid", "dirty", "focused",
2215 "touched", "checked", "open", "ready", "submitted"};
2216 static constexpr std::array<std::string_view, 3> kCounts = {"viewCount", "sourceCount", "selectedCount"};
2217 if (listed(kBools, value) || value == "pending") {
2218 return Type::of(TypeKind::Bool);
2219 }
2220 if (value == "errors" || value == "shownErrors") {
2222 }
2223 if (listed(kCounts, value)) {
2224 return Type::of(TypeKind::Int);
2225 }
2226 if (value == "error") {
2227 return Type::of(TypeKind::String, true);
2228 }
2229 return Type::any();
2230}
2231
2232/// @brief Whether a value of type @p actual may stand where @p declared is expected.
2233/// @param declared The expected type.
2234/// @param actual The type found.
2235/// @return Whether they are compatible: equal kinds, `any` on either side, an `int` for a `decimal` or `key`, a
2236/// `string` for a `key`.
2237[[nodiscard]] inline bool compatible(Type const& declared, Type const& actual) {
2238 if (declared.isAny() || actual.isAny() || declared.kind == actual.kind) {
2239 return true;
2240 }
2241 bool const integer = actual.kind == TypeKind::Int;
2242 return (declared.kind == TypeKind::Decimal && integer) ||
2243 (declared.kind == TypeKind::Key && (integer || actual.kind == TypeKind::String));
2244}
2245
2246// NOLINTBEGIN(misc-no-recursion): a document is a tree of scopes, nodes and command lists, and checking descends it.
2247/// @brief Checks a parsed document against its own names and the catalog: every load-time rule that needs more
2248/// than one place in the document.
2250public:
2251 /// @brief Validates @p doc.
2252 /// @param doc The parsed document.
2253 /// @param options The catalog facts.
2254 DocumentValidator(Document const& doc, LoadOptions const& options) : _doc{&doc}, _options{&options} {}
2255
2256 /// @brief Runs every rule; throws `DocumentRefused` on the first refusal.
2257 void run() {
2258 ScopeInfo& screen = newScope(nullptr, false);
2259 screen.screen = true;
2260 for (auto const& [name, type] : _doc->params) {
2261 NameEntry entry;
2263 entry.path = pointerAppend("/params", name);
2264 entry.type = type;
2265 add(screen, name, std::move(entry));
2266 }
2267 declare(_doc->scope, screen);
2268 for (auto const& [key, commands] : _doc->lifecycle) {
2269 static_cast<void>(key);
2270 collectTransfers(commands, screen);
2271 }
2272 for (auto const& [chord, commands] : _doc->keys) {
2273 static_cast<void>(chord);
2274 collectTransfers(commands, screen);
2275 }
2276 if (_doc->view) {
2277 collect(*_doc->view, screen);
2278 }
2279 for (DocNodePtr const& part : headParts()) {
2280 collectPart(*part, screen, screen);
2281 }
2282 std::vector<ScopeInfo*> componentScopes;
2283 for (auto const& [name, component] : _doc->components) {
2284 static_cast<void>(name);
2285 ScopeInfo& fallbackScope = newScope(nullptr, false);
2286 collect(*component.fallback, fallbackScope);
2287 componentScopes.push_back(&fallbackScope);
2288 }
2289
2290 CheckContext const root{
2291 .scope = &screen, .commands = false, .boundaryError = false, .onClose = false, .bindings = {}};
2292 checkScope(_doc->scope, root);
2293 if (_doc->title) {
2294 checkExpr(*_doc->title, root);
2295 }
2296 if (_doc->identity) {
2297 checkExpr(*_doc->identity, root);
2298 }
2299 if (_doc->dirty) {
2300 checkCondition(*_doc->dirty, root, "`dirty`");
2301 }
2302 if (_doc->subtitle) {
2303 checkExpr(*_doc->subtitle, root);
2304 }
2305 for (DocNodePtr const& part : headParts()) {
2306 checkElement(*part, root, true);
2307 }
2308 checkActionButtons(_doc->actions, "/actions");
2309 for (auto const& [key, commands] : _doc->lifecycle) {
2310 CheckContext context = root;
2311 context.onClose = key == "onClose";
2312 checkCommands(commands, context);
2313 }
2314 for (auto const& [chord, commands] : _doc->keys) {
2315 static_cast<void>(chord);
2316 checkCommands(commands, root);
2317 }
2318 checkPrefetch();
2319 if (_doc->view) {
2320 checkNode(*_doc->view, root);
2321 }
2322 std::size_t index = 0;
2323 for (auto const& [name, component] : _doc->components) {
2324 static_cast<void>(name);
2325 CheckContext context{.scope = componentScopes.at(index++),
2326 .commands = false,
2327 .boundaryError = false,
2328 .onClose = false,
2329 .bindings = {}};
2330 context.bindings.emplace_back("props", Type::of(TypeKind::Record));
2331 checkNode(*component.fallback, context);
2332 }
2333 checkCustomizePoints();
2334 checkWatchGraph();
2335 checkEffects();
2336 }
2337
2338 /// @brief What validation found that does not refuse the document.
2339 /// @return The warnings, in the order found; the validator keeps none.
2340 [[nodiscard]] std::vector<LoadWarning> takeWarnings() noexcept { return std::move(_warnings); }
2341
2342 /// @brief The static type of each date input's `value` that the checker could decide.
2343 /// @return The table, keyed by node; the validator keeps none.
2344 [[nodiscard]] std::map<DocNode const*, TypeKind> takeDateValueKinds() noexcept {
2345 return std::move(_dateValueKinds);
2346 }
2347
2348 /// @brief Finds a name in a scope chain.
2349 /// @param scope The innermost scope.
2350 /// @param name The name.
2351 /// @return The entry, or `nullptr`.
2352 [[nodiscard]] static NameEntry* lookup(ScopeInfo* scope, std::string_view name) {
2353 for (ScopeInfo* current = scope; current != nullptr; current = current->parent) {
2354 auto const found = current->names.find(name);
2355 if (found != current->names.end()) {
2356 return &found->second;
2357 }
2358 }
2359 return nullptr;
2360 }
2361
2362 /// @brief The type of a name whose type is derived from an expression, derived on first use.
2363 /// @param entry The state or `let` entry.
2364 /// @param name Its name, for the cycle's description.
2365 /// @return The type, or the cycle it closes.
2366 [[nodiscard]] std::expected<Type, std::string> derivedType(NameEntry& entry, std::string_view name) {
2367 if (entry.visit == 2 || !entry.derivedFrom) {
2368 return entry.type;
2369 }
2370 if (entry.visit == 1) {
2371 std::string cycle;
2372 auto const start = std::ranges::find(_deriving, name);
2373 for (auto step = start; step != _deriving.end(); ++step) {
2374 cycle += *step + " → ";
2375 }
2376 std::string const graph =
2377 entry.category == NameCategory::Let ? "the let graph has a cycle: " : "the state inits form a cycle: ";
2378 return std::unexpected(graph + cycle + std::string{name});
2379 }
2380 entry.visit = 1;
2381 _deriving.emplace_back(name);
2382 CheckContext const context{
2383 .scope = entry.scope, .commands = false, .boundaryError = false, .onClose = false, .bindings = {}};
2384 Type type = checkExpr(*entry.derivedFrom, context);
2385 _deriving.pop_back();
2386 entry.visit = 2;
2387 entry.type = type;
2388 return type;
2389 }
2390
2391 /// @brief The document.
2392 /// @return It.
2393 [[nodiscard]] Document const& document() const noexcept { return *_doc; }
2394
2395private:
2396 ScopeInfo& newScope(ScopeInfo* parent, bool row) {
2397 ScopeInfo& made = _scopes.emplace_back();
2398 made.parent = parent;
2399 made.row = row;
2400 return made;
2401 }
2402
2403 static void add(ScopeInfo& scope, std::string const& name, NameEntry entry) {
2404 static constexpr std::array<std::string_view, 6> kReserved = {"event", "app", "boundary",
2405 "props", "result", "body"};
2406 if (listed(kReserved, name)) {
2407 refuse(entry.path, "`" + name + "` is reserved and cannot be declared");
2408 }
2409 auto const found = scope.names.find(name);
2410 if (found != scope.names.end()) {
2411 refuse(entry.path, "`" + name + "` is declared twice in this scope (first at " + found->second.path + ")");
2412 }
2413 entry.scope = &scope;
2414 scope.names.emplace(name, std::move(entry));
2415 }
2416
2417 /// Checks a model alias against the catalog's facts, when there is a catalog.
2418 void checkCatalog(ModelDecl const& model) {
2419 if (!_options->catalog.model) {
2420 return;
2421 }
2422 auto const info = _options->catalog.model(model.type);
2423 if (!info) {
2424 refuse(pointerAppend(model.path, "type"), "the model type `" + model.type + "` is not in the catalog");
2425 }
2426 if (model.instance == InstanceMode::Shared && !info->stateless) {
2427 refuse(pointerAppend(model.path, "instance"),
2428 "the model type `" + model.type +
2429 "` keeps state, so `shared` names no instance: it needs a key, or `private`");
2430 }
2431 if (model.instance == InstanceMode::Keyed && !info->keyed) {
2432 refuse(pointerAppend(model.path, "instance"),
2433 "the model type `" + model.type + "` is not keyed, so its instance is not an expression");
2434 }
2435 }
2436
2437 void declare(ScopeDecl const& decl, ScopeInfo& scope) {
2438 for (ModelDecl const& model : decl.models) {
2439 if (scope.row && model.instance == InstanceMode::Default) {
2440 refuse(model.path, "a model alias in a row scope names its `instance`");
2441 }
2442 if (!scope.screen && model.instance == InstanceMode::Borrowed) {
2443 refuse(model.path, "only a screen's own aliases borrow the opener's instance");
2444 }
2445 checkCatalog(model);
2446 NameEntry entry;
2447 entry.category = NameCategory::Model;
2448 entry.path = model.path;
2449 entry.model = &model;
2450 add(scope, model.name, std::move(entry));
2451 }
2452 for (StateDecl const& state : decl.state) {
2453 NameEntry entry;
2454 entry.category = NameCategory::State;
2455 entry.path = state.path;
2456 entry.type = state.type;
2457 entry.derivedFrom = state.type.isAny() ? state.init : nullptr;
2458 add(scope, state.name, std::move(entry));
2459 }
2460 for (LetDecl const& let : decl.lets) {
2461 NameEntry entry;
2462 entry.category = NameCategory::Let;
2463 entry.path = let.path;
2464 entry.derivedFrom = let.value;
2465 add(scope, let.name, std::move(entry));
2466 }
2467 for (QueryDecl const& query : decl.queries) {
2468 NameEntry entry;
2469 entry.category = NameCategory::Query;
2470 entry.path = query.path;
2471 entry.query = &query;
2472 add(scope, query.name, std::move(entry));
2473 _queries.emplace(query.name, &query);
2474 _queryScopes.emplace_back(&query, &scope);
2475 }
2476 for (MutationDecl const& mutation : decl.mutations) {
2477 NameEntry entry;
2478 entry.category = NameCategory::Mutation;
2479 entry.path = mutation.path;
2480 entry.mutation = &mutation;
2481 add(scope, mutation.name, std::move(entry));
2482 _mutations.emplace_back(&mutation, &scope);
2483 collectTransfers(mutation.onSuccess, scope);
2484 collectTransfers(mutation.onError, scope);
2485 }
2486 for (WatchDecl const& watch : decl.watches) {
2487 _watches.emplace_back(&watch, &scope);
2488 collectTransfers(watch.body, scope);
2489 }
2490 }
2491
2492 void collectTransfers(Commands const& commands, ScopeInfo& scope) {
2493 for (Command const& cmd : commands) {
2494 if (cmd.kind == CommandKind::Upload && !cmd.transfer.empty()) {
2495 NameEntry entry;
2496 entry.category = NameCategory::Transfer;
2497 entry.path = pointerAppend(cmd.path, "id");
2498 add(scope, cmd.transfer, std::move(entry));
2499 }
2500 collectTransfers(cmd.commands, scope);
2501 collectTransfers(cmd.alternative, scope);
2502 }
2503 }
2504
2505 void collect(DocNode const& node, ScopeInfo& scope) { collectIn(node, scope); }
2506
2507 void collectIn(DocNode const& node, ScopeInfo& scope) {
2508 KindSpec const* const spec = findKind(node.kind, false);
2509 if (!node.id.empty()) {
2510 NameEntry entry;
2511 entry.category = NameCategory::Element;
2512 entry.path = pointerAppend(node.path, "id");
2513 entry.element = &node;
2514 add(scope, node.id, std::move(entry));
2515 _elements.emplace(node.id, &node);
2516 }
2517 ScopeInfo* inner = &scope;
2518 if (spec != nullptr && (spec->flags & kScope) != 0) {
2519 inner = &newScope(&scope, (spec->flags & kRowScope) != 0);
2520 _scopeOf.emplace(&node, inner);
2521 // NOLINTNEXTLINE(bugprone-unchecked-optional-access): the parser gives every scope-creating node a scope.
2522 declare(*node.scope, *inner);
2523 }
2524 _innerOf.emplace(&node, inner);
2525 for (auto const& [key, commands] : node.events) {
2526 collectTransfers(commands, isInner(spec, key) ? *inner : scope);
2527 }
2528 for (auto const& [chord, commands] : node.keys) {
2529 static_cast<void>(chord);
2530 collectTransfers(commands, scope);
2531 }
2532 for (auto const& [key, nodes] : node.children) {
2533 for (DocNodePtr const& child : nodes) {
2534 collectIn(*child, isInner(spec, key) ? *inner : scope);
2535 }
2536 }
2537 for (auto const& [key, parts] : node.parts) {
2538 for (DocNodePtr const& child : parts) {
2539 collectPart(*child, isInner(spec, key) ? *inner : scope, *inner);
2540 }
2541 }
2542 }
2543
2544 void collectPart(DocNode const& part, ScopeInfo& outer, ScopeInfo& ownerInner) {
2545 KindSpec const* const spec = findKind(part.kind, true);
2546 ScopeInfo* inner = &ownerInner;
2547 if (spec != nullptr && (spec->flags & kScope) != 0) {
2548 inner = &newScope(&outer, false);
2549 _scopeOf.emplace(&part, inner);
2550 // NOLINTNEXTLINE(bugprone-unchecked-optional-access): the parser gives every switch case a scope.
2551 declare(*part.scope, *inner);
2552 }
2553 _innerOf.emplace(&part, inner);
2554 for (auto const& [key, commands] : part.events) {
2555 collectTransfers(commands, outer);
2556 }
2557 for (auto const& [key, nodes] : part.children) {
2558 for (DocNodePtr const& child : nodes) {
2559 collectIn(*child, isInner(spec, key) ? *inner : outer);
2560 }
2561 }
2562 for (auto const& [key, parts] : part.parts) {
2563 static_cast<void>(key);
2564 for (DocNodePtr const& child : parts) {
2565 collectPart(*child, outer, ownerInner);
2566 }
2567 }
2568 }
2569
2570 [[nodiscard]] static bool isInner(KindSpec const* spec, std::string_view key) {
2571 if (spec == nullptr) {
2572 return false;
2573 }
2574 auto const found = std::ranges::find(spec->keys, key, &KeySpec::key);
2575 return found != spec->keys.end() && (found->flags & kInner) != 0;
2576 }
2577
2578 /// The flags of a child-node key. The parser fills `children` only from a kind's own keys, so the kind and the key
2579 /// are always found.
2580 [[nodiscard]] static std::uint8_t keyFlags(KindSpec const& spec, std::string_view key) {
2581 return std::ranges::find(spec.keys, key, &KeySpec::key)->flags;
2582 }
2583
2584 [[nodiscard]] static bool isCondition(KindSpec const* spec, std::string_view key) {
2585 auto shapeOf = [key](std::span<KeySpec const> keys) {
2586 auto const found = std::ranges::find(keys, key, &KeySpec::key);
2587 return found == keys.end() ? Shape::Expr : found->shape;
2588 };
2589 return (spec != nullptr && shapeOf(spec->keys) == Shape::Condition) ||
2590 shapeOf(kCommonKeys) == Shape::Condition || shapeOf(kFieldKeys) == Shape::Condition;
2591 }
2592
2593 Type checkExpr(Expr const& expr, CheckContext const& context) {
2594 ScopeResolver const resolver{*this, context};
2595 auto checked = checkExpression(expr, resolver);
2596 if (!checked) {
2597 throw DocumentRefused{std::move(checked.error())};
2598 }
2599 return std::move(*checked);
2600 }
2601
2602 void checkCondition(Expr const& expr, CheckContext const& context, std::string_view what) {
2603 Type const type = checkExpr(expr, context);
2604 if (!compatible(Type::of(TypeKind::Bool), type)) {
2605 refuse(expr.path, std::string{what} + " is " + type.name() + "; expected bool");
2606 }
2607 }
2608
2609 // A date input shows a timestamp or an ISO-8601 date or date-time string; any other type it can never show.
2610 void checkDateValue(DocNode const& node, Expr const& expr, Type const& type) {
2611 if (type.kind == TypeKind::String || type.kind == TypeKind::Timestamp) {
2612 _dateValueKinds.insert_or_assign(&node, type.kind);
2613 } else if (type.kind != TypeKind::Any && type.kind != TypeKind::Key) {
2614 refuse(expr.path,
2615 "`dateTimeInput` shows a timestamp or an ISO-8601 date or date-time string; `value` is " +
2616 type.name());
2617 }
2618 }
2619
2620 [[nodiscard]] static NameEntry& require(CheckContext const& context, std::string const& name,
2621 std::string const& path, std::initializer_list<NameCategory> categories,
2622 std::string_view what) {
2623 NameEntry* const entry = lookup(context.scope, name);
2624 if (entry == nullptr || std::ranges::find(categories, entry->category) == categories.end()) {
2625 refuse(path, "`" + name + "` is not " + std::string{what} + " of this scope");
2626 }
2627 return *entry;
2628 }
2629
2630 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): the alias, then the action it runs, as a query reads.
2631 std::optional<ActionInfo> checkModelAction(CheckContext const& context, std::string const& alias,
2632 std::string const& action, std::string const& path) {
2633 std::string const modelPath = pointerAppend(path, "model");
2634 NameEntry const& model = require(context, alias, modelPath, {NameCategory::Model}, "a model alias");
2635 if (!_options->catalog.action) {
2636 return std::nullopt;
2637 }
2638 auto const info = _options->catalog.action(action);
2639 if (!info) {
2640 refuse(path, "the action `" + action + "` is not in the catalog");
2641 }
2642 if (info->modelType != model.model->type) {
2643 refuse(path, "the action `" + action + "` belongs to `" + info->modelType + "`, not `" +
2644 model.model->type + "`");
2645 }
2646 return info;
2647 }
2648
2649 void checkScope(ScopeDecl const& decl, CheckContext const& context) {
2650 for (ModelDecl const& model : decl.models) {
2651 if (model.key) {
2652 checkExpr(*model.key, context);
2653 }
2654 }
2655 for (StateDecl const& state : decl.state) {
2656 if (!state.init) {
2657 continue;
2658 }
2659 Type const type = checkExpr(*state.init, context);
2660 bool const literalFits = state.init->kind != ExprKind::Literal || state.type.admits(state.init->literal);
2661 if (!literalFits || !compatible(state.type, type)) {
2662 refuse(state.init->path,
2663 "the init of `" + state.name + "` does not have its type " + state.type.name());
2664 }
2665 // The type is derived now, so a state no expression reads is still checked.
2666 auto const derived = derivedType(*lookup(context.scope, state.name), state.name);
2667 static_cast<void>(derived);
2668 }
2669 for (LetDecl const& let : decl.lets) {
2670 auto type = derivedType(*lookup(context.scope, let.name), let.name);
2671 static_cast<void>(type);
2672 }
2673 for (QueryDecl const& query : decl.queries) {
2674 checkQuery(query, context);
2675 }
2676 for (MutationDecl const& mutation : decl.mutations) {
2677 checkMutation(mutation, context);
2678 }
2679 for (WatchDecl const& watch : decl.watches) {
2680 checkExpr(*watch.on, context);
2681 checkCommands(watch.body, context);
2682 }
2683 for (CheckDecl const& check : decl.checks) {
2684 checkCondition(*check.assertion, context, "the `assert` of a check");
2685 if (check.message) {
2686 checkExpr(*check.message, context);
2687 }
2688 for (std::size_t index = 0; index < check.targets.size(); ++index) {
2689 static_cast<void>(require(context, check.targets.at(index),
2690 pointerAppend(pointerAppend(check.path, "targets"), index),
2691 {NameCategory::Element}, "an element"));
2692 }
2693 }
2694 }
2695
2696 void checkQuery(QueryDecl const& query, CheckContext const& context) {
2697 auto const action = checkModelAction(context, query.model, query.action, query.path);
2698 // An evaluation runs no handler, so only a call can change anything.
2699 if (action && action->effects && !query.evaluate) {
2700 refuse(query.path, std::format("a query reads, so its action declares no effects; `{}` declares {}, "
2701 "so it runs as a mutation",
2702 query.action, describeEffects(*action->effects)));
2703 }
2704 if (query.body) {
2705 checkExpr(*query.body, context);
2706 }
2707 if (query.when) {
2708 checkCondition(*query.when, context, "`when`");
2709 }
2710 }
2711
2712 void checkMutation(MutationDecl const& mutation, CheckContext const& context) {
2713 auto const action = checkModelAction(context, mutation.model, mutation.action, mutation.path);
2714 if (mutation.body) {
2715 checkExpr(*mutation.body, context);
2716 }
2717 if (mutation.serialKey) {
2718 CheckContext keyContext = context;
2719 keyContext.bindings.emplace_back("body", Type::any());
2720 checkExpr(*mutation.serialKey, keyContext);
2721 }
2722 if (mutation.optimistic) {
2723 OptimisticDecl const& decl = *mutation.optimistic;
2724 std::string const path = pointerAppend(mutation.path, "optimistic");
2725 auto const target = _queries.find(decl.query);
2726 if (target == _queries.end()) {
2727 refuse(pointerAppend(path, "query"), "`" + decl.query + "` is not a query of this document");
2728 }
2729 CheckContext bodyContext = context;
2730 bodyContext.bindings.emplace_back("body", Type::any());
2731 checkExpr(*decl.key, bodyContext);
2732 checkExpr(*decl.with, bodyContext);
2733 checkPatchAt(decl.query, decl.at, *target->second, path);
2734 checkOptimistic(decl, *target->second);
2735 }
2736 if (mutation.offlineQueue && action) {
2737 if (action->credential || !action->secretFields.empty()) {
2738 refuse(pointerAppend(mutation.path, "offline"),
2739 "a mutation that sends a credential or returns a secret is never queued offline");
2740 }
2741 }
2742 checkCommands(mutation.onSuccess, context);
2743 checkCommands(mutation.onError, context);
2744 }
2745
2746 // Only a catalog that knows the result is a record can say a patch misses its list; otherwise the interpreter
2747 // reports the miss when the patch runs.
2748 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): the query's name, then the member holding the list.
2749 void checkPatchAt(std::string const& queryName, std::string const& listMember, QueryDecl const& query,
2750 std::string const& path) {
2751 if (!_options->catalog.action) {
2752 return;
2753 }
2754 auto const info = _options->catalog.action(query.action);
2755 if (!info || !info->listFields) {
2756 return;
2757 }
2758 if (listMember.empty()) {
2759 refuse(path, std::format("`{}` answers a record, so a patch of `{}` names the member holding its list "
2760 "with `at`",
2761 query.action, queryName));
2762 }
2763 if (std::ranges::find(*info->listFields, listMember) == info->listFields->end()) {
2764 refuse(pointerAppend(path, "at"),
2765 std::format("the result of `{}` has no list member `{}`", query.action, listMember));
2766 }
2767 }
2768
2769 void checkOptimistic(OptimisticDecl const& decl, QueryDecl const& query) {
2770 if (!_options->catalog.action || decl.with->kind != ExprKind::Object) {
2771 return;
2772 }
2773 auto const info = _options->catalog.action(query.action);
2774 if (!info) {
2775 return;
2776 }
2777 for (auto const& [field, value] : decl.with->fields) {
2778 if (std::ranges::find(info->calculatedFields, field) != info->calculatedFields.end()) {
2779 refuse(value->path,
2780 std::format("`{}` is calculated by the server and is never written optimistically", field));
2781 }
2782 }
2783 }
2784
2785 void checkPrefetch() {
2786 for (std::size_t index = 0; index < _doc->prefetch.size(); ++index) {
2787 std::string const& name = _doc->prefetch.at(index);
2788 std::string const path = pointerAppend("/prefetch", index);
2789 auto const found = std::ranges::find(_doc->scope.queries, name, &QueryDecl::name);
2790 if (found == _doc->scope.queries.end()) {
2791 refuse(path, "`" + name + "` is not a query of the screen");
2792 }
2793 if (found->body && !readsOnlyParams(*found->body)) {
2794 refuse(path, "the body of `" + name + "` reads more than the screen's params, so it cannot prefetch");
2795 }
2796 }
2797 }
2798
2799 [[nodiscard]] bool readsOnlyParams(Expr const& expr) const {
2800 if (expr.kind == ExprKind::Ref) {
2801 return std::ranges::find(_doc->params, expr.name, &std::pair<std::string, Type>::first) !=
2802 _doc->params.end();
2803 }
2804 if (expr.kind != ExprKind::Literal && expr.kind != ExprKind::Call && expr.kind != ExprKind::If &&
2805 expr.kind != ExprKind::Object && expr.kind != ExprKind::List) {
2806 return false;
2807 }
2808 bool only = true;
2809 for (ExprPtr const& arg : expr.args) {
2810 only = only && readsOnlyParams(*arg);
2811 }
2812 for (auto const& [name, field] : expr.fields) {
2813 static_cast<void>(name);
2814 only = only && readsOnlyParams(*field);
2815 }
2816 return only;
2817 }
2818
2819 void checkCustomizePoints() {
2820 std::set<std::string, std::less<>> arranged;
2821 for (auto const& [name, point] : _doc->customize) {
2822 static_cast<void>(name);
2823 if (point.kind == "columns" && _doc->view) {
2824 auto const found = _elements.find(point.of);
2825 if (found == _elements.end() || found->second->kind != "table") {
2826 refuse(pointerAppend(point.path, "of"),
2827 "a `columns` point names a table; `" + point.of + "` is not one");
2828 }
2829 }
2830 if (point.kind == "arrange") {
2831 checkArranged(point, arranged);
2832 }
2833 }
2834 }
2835
2836 // The stored order names children by id, so the container is a stack whose every child has one, and one point
2837 // orders it: two would each claim the order.
2838 void checkArranged(CustomizePoint const& point, std::set<std::string, std::less<>>& arranged) {
2839 auto const found = _elements.find(point.of);
2840 if (found == _elements.end() || (found->second->kind != "column" && found->second->kind != "row") ||
2841 found->second->nodes("children").empty()) {
2842 refuse(pointerAppend(point.path, "of"),
2843 "an `arrange` point names a column or row of this screen that has children; `" + point.of +
2844 "` is not one");
2845 }
2846 for (DocNodePtr const& child : found->second->nodes("children")) {
2847 if (child->id.empty()) {
2848 refuse(pointerAppend(point.path, "of"),
2849 "an `arrange` point orders its container's children by id; a child of `" + point.of +
2850 "` has none");
2851 }
2852 }
2853 // An arranged container is drawn as a keyed list, which places its children as a list does.
2854 std::string const asList =
2855 "an arranged `" + found->second->kind + "` is drawn as a list, which takes no `align` or `justify`";
2856 for (std::string_view const key : {"align", "justify"}) {
2857 if (!found->second->name(key).empty()) {
2858 refuse(pointerAppend(found->second->path, key), asList);
2859 }
2860 }
2861 if (!arranged.insert(point.of).second) {
2862 refuse(pointerAppend(point.path, "of"), "`" + point.of + "` is arranged by another point");
2863 }
2864 }
2865
2866 [[nodiscard]] CheckContext innerContext(DocNode const& node, CheckContext const& context) const {
2867 CheckContext inner = context;
2868 inner.scope = _innerOf.at(&node);
2869 return inner;
2870 }
2871
2872 void checkNode(DocNode const& node, CheckContext const& context) {
2873 if (!node.name("alignSelf").empty()) {
2874 refuse(pointerAppend(node.path, "alignSelf"), std::string{kAlignSelfPlaces});
2875 }
2876 checkElement(node, context, false);
2877 }
2878
2879 // What `alignSelf` places a node in.
2880 static constexpr std::string_view kAlignSelfPlaces = "`alignSelf` places a node in its row, column or grid";
2881
2882 // A child that places itself sits in a row, a column, a grid or a `forEach`; any other parent (a panel, a dialog,
2883 // a tab page) has no cross axis to place it on.
2884 static void checkPlacements(DocNode const& parent) {
2885 bool const places =
2886 parent.kind == "row" || parent.kind == "column" || parent.kind == "grid" || parent.kind == "forEach";
2887 for (auto const& [key, nodes] : parent.children) {
2888 for (DocNodePtr const& child : nodes) {
2889 if (!child->name("alignSelf").empty() && (!places || key != "children")) {
2890 refuse(pointerAppend(child->path, "alignSelf"), std::string{kAlignSelfPlaces});
2891 }
2892 }
2893 }
2894 }
2895
2896 void checkElement(DocNode const& node, CheckContext const& outer, bool part) {
2897 KindSpec const* const spec = findKind(node.kind, part);
2898 CheckContext const inner = innerContext(node, outer);
2899 if (auto const found = _scopeOf.find(&node); found != _scopeOf.end()) {
2900 // NOLINTNEXTLINE(bugprone-unchecked-optional-access): a node or case with a scope of its own has its declarations.
2901 checkScope(*node.scope, inner);
2902 }
2903 checkProps(node, spec, inner, outer);
2904 for (auto const& [key, commands] : node.events) {
2905 if (node.kind == "custom") {
2906 checkCustomEvent(node, key.substr(3), node.path);
2907 }
2908 checkCommands(commands, isInner(spec, key) ? inner : outer);
2909 }
2910 for (auto const& [chord, commands] : node.keys) {
2911 static_cast<void>(chord);
2912 checkCommands(commands, outer);
2913 }
2914 for (auto const& [key, nodes] : node.children) {
2915 CheckContext context = isInner(spec, key) ? inner : outer;
2916 context.boundaryError = (keyFlags(*spec, key) & kBoundaryError) != 0 || context.boundaryError;
2917 for (DocNodePtr const& child : nodes) {
2918 checkElement(*child, context, false);
2919 }
2920 }
2921 for (auto const& [key, parts] : node.parts) {
2922 CheckContext const& context = isInner(spec, key) ? inner : outer;
2923 for (DocNodePtr const& child : parts) {
2924 checkElement(*child, context, true);
2925 }
2926 }
2927 checkPlacements(node);
2928 if (!part) {
2929 checkFlags(node);
2930 checkLayoutKeys(node);
2931 checkKindRules(node, outer);
2932 // An overlay's footer actions; a form's field components.
2933 checkActionButtons(node.partList("actions"), pointerAppend(node.path, "actions"));
2934 checkFieldComponents(node);
2935 }
2936 }
2937
2938 void checkProps(DocNode const& node, KindSpec const* spec, CheckContext const& inner, CheckContext const& outer) {
2939 for (auto const& [key, expr] : node.props) {
2940 CheckContext const& context = isInner(spec, key) ? inner : outer;
2941 if (isCondition(spec, key)) {
2942 checkCondition(*expr, context, "`" + key + "`");
2943 continue;
2944 }
2945 Type const type = checkExpr(*expr, context);
2946 if (node.kind == "dateTimeInput" && key == "value") {
2947 checkDateValue(node, *expr, type);
2948 }
2949 }
2950 }
2951
2952 void checkCustomEvent(DocNode const& node, std::string const& event, std::string const& path) {
2953 auto const found = _doc->components.find(node.name("name"));
2954 if (found != _doc->components.end() && !found->second.events.contains(event)) {
2955 refuse(pointerAppend(pointerAppend(path, "on"), event),
2956 "the component `" + found->second.name + "` declares no event `" + event + "`");
2957 }
2958 }
2959
2960 // A flag that needs a key beside it.
2961 static void checkFlags(DocNode const& node) {
2962 if (node.flag("showLabel") && node.prop("label") == nullptr) {
2963 refuse(pointerAppend(node.path, "showLabel"), "`showLabel` shows the `label`, and the node gives none");
2964 }
2965 }
2966
2967 void checkKindRules(DocNode const& node, CheckContext const& context) {
2968 if (node.kind == "custom") {
2969 checkCustomProps(node);
2970 } else if (node.kind == "customize" && !_doc->customize.contains(node.name("point"))) {
2971 refuse(pointerAppend(node.path, "point"),
2972 "`" + std::string{node.name("point")} + "` is not a customization point of this document");
2973 }
2974 checkKindValues(node);
2975 std::string_view const model = node.name("model");
2976 if (!model.empty()) {
2977 if (node.name("action").empty()) {
2978 refuse(node.path, "a `" + node.kind + "` with a `model` names its `action`");
2979 }
2980 checkModelAction(context, std::string{model}, std::string{node.name("action")}, node.path);
2981 } else if (node.kind == "table" && !node.prop("rows")) {
2982 refuse(node.path, "a table takes `rows`, or a `model` and `action`");
2983 }
2984 }
2985
2986 // A custom node names a component of the document and gives only the props it declares.
2987 void checkCustomProps(DocNode const& node) const {
2988 auto const found = _doc->components.find(node.name("name"));
2989 if (found == _doc->components.end()) {
2990 refuse(pointerAppend(node.path, "name"),
2991 "`" + std::string{node.name("name")} + "` is not a component of this document");
2992 }
2993 for (DocNodePtr const& props : node.partList("props")) {
2994 for (auto const& [prop, value] : props->props) {
2995 if (!found->second.props.contains(prop)) {
2996 refuse(value->path, "the component `" + found->first + "` declares no prop `" + prop + "`");
2997 }
2998 }
2999 }
3000 }
3001
3002 // The keys of a kind that take one of a closed set of values, and the pairs of keys that need each other.
3003 static void checkKindValues(DocNode const& node) {
3004 if (node.kind == "button") {
3005 static constexpr std::array<std::string_view, 4> kEmphasis = {"normal", "primary", "danger", "ghost"};
3006 checkChoice(node, "emphasis", kEmphasis);
3007 static constexpr std::array<std::string_view, 2> kStyles = {"push", "item"};
3008 checkChoice(node, "style", kStyles);
3009 if (node.prop("selected") != nullptr && node.name("style") != "item") {
3010 refuse(pointerAppend(node.path, "selected"),
3011 "`selected` marks a list item; give the button `style: item`");
3012 }
3013 } else if (node.kind == "grid" && !node.name("justify").empty()) {
3014 refuse(pointerAppend(node.path, "justify"),
3015 "a grid's columns share its width; there is no spare length to justify");
3016 } else if (node.kind == "textInput") {
3017 static constexpr std::array<std::string_view, 3> kCommit = {"change", "blur", "enter"};
3018 checkChoice(node, "commitOn", kCommit);
3019 } else if (node.kind == "select") {
3020 static constexpr std::array<std::string_view, 2> kStale = {"clear", "keep"};
3021 checkChoice(node, "onStale", kStale);
3022 } else if (node.kind == "dialog" || node.kind == "drawer" || node.kind == "form") {
3023 static constexpr std::array<std::string_view, 3> kPolicies = {"touched", "submit", "always"};
3024 checkChoice(node, "showErrors", kPolicies);
3025 // Absent keeps the schema's own mode, so a misspelt mode would silently do the same.
3026 static constexpr std::array<std::string_view, 2> kSubmitModes = {"explicit", "auto"};
3027 checkChoice(node, "submit", kSubmitModes);
3028 } else if (node.kind == "text") {
3029 checkTextRole(node);
3030 }
3031 if (node.kind == "form" || node.kind == "collection" || node.kind == "wizard") {
3032 checkFormColumns(node);
3033 static constexpr std::array<std::string_view, 2> kFieldLayouts = {"stacked", "inline"};
3034 checkChoice(node, "fieldLayout", kFieldLayouts);
3035 }
3036 static constexpr std::array<std::string_view, 4> kAlign = {"start", "center", "end", "stretch"};
3037 static constexpr std::array<std::string_view, 4> kJustify = {"start", "center", "end", "between"};
3038 checkChoice(node, "align", kAlign);
3039 checkChoice(node, "justify", kJustify);
3040 checkChoice(node, "alignSelf", kAlign);
3041 }
3042
3043 // Each form field drawn by a host component.
3044 void checkFieldComponents(DocNode const& form) const {
3045 if (form.kind != "form") {
3046 return;
3047 }
3048 for (DocNodePtr const& record : form.partList("fields")) {
3049 for (DocNodePtr const& field : record->partList("fields")) {
3050 if (!field->name("component").empty()) {
3051 checkFieldComponent(*field);
3052 }
3053 }
3054 }
3055 }
3056
3057 // A form field drawn by a host component: a declared one with a `value` prop and a `change` event, given only
3058 // props it declares, and not beside a `widget`, which would choose the control too.
3059 void checkFieldComponent(DocNode const& field) const {
3060 std::string_view const name = field.name("component");
3061 std::string const where = pointerAppend(field.path, "component");
3062 auto const found = _doc->components.find(name);
3063 if (found == _doc->components.end()) {
3064 refuse(where, "`" + std::string{name} + "` is not a component of this document");
3065 }
3066 Component const& component = found->second;
3067 if (!component.props.contains("value") || !component.events.contains("change")) {
3068 refuse(where, "the component `" + component.name +
3069 "` draws a form field, so it declares a prop `value` and an event `change`");
3070 }
3071 if (field.prop("widget") != nullptr) {
3072 refuse(where, "`component` and `widget` both choose the field's control; give one");
3073 }
3074 for (DocNodePtr const& props : field.partList("props")) {
3075 for (auto const& [prop, value] : props->props) {
3076 if (!component.props.contains(prop)) {
3077 refuse(value->path, "the component `" + component.name + "` declares no prop `" + prop + "`");
3078 }
3079 }
3080 }
3081 }
3082
3083 // The screen head's parts: its status badges, then its actions.
3084 [[nodiscard]] std::vector<DocNodePtr> headParts() const {
3085 std::vector<DocNodePtr> parts = _doc->status;
3086 parts.insert(parts.end(), _doc->actions.begin(), _doc->actions.end());
3087 return parts;
3088 }
3089
3090 // A list of `actionButton` parts: each id once, and each emphasis one of a button's.
3091 static void checkActionButtons(std::span<DocNodePtr const> buttons, std::string const& path) {
3092 static constexpr std::array<std::string_view, 4> kEmphasis = {"normal", "primary", "danger", "ghost"};
3093 std::set<std::string_view> ids;
3094 for (DocNodePtr const& button : buttons) {
3095 if (!ids.insert(button->id).second) {
3096 refuse(pointerAppend(button->path, "id"),
3097 "the action id `" + button->id + "` is used twice in " + path + "; each action has its own");
3098 }
3099 checkChoice(*button, "emphasis", kEmphasis);
3100 }
3101 }
3102
3103 // A literal sizing of one dimension: `"content"`, `{"fixed": n}` (n of 0 or more) or `{"stretch": w}` (w of 1 or
3104 // more). Answers the fixed amount, if it is one.
3105 static std::optional<std::int64_t> sizingDimension(Value const& value, std::string const& path) {
3106 static constexpr std::string_view kForms =
3107 "`sizing` is `\"content\"`, `{\"fixed\": n}`, `{\"stretch\": w}`, or a record of `width` and `height` "
3108 "of them";
3109 if (std::string const* const text = value.asString(); text != nullptr && *text == "content") {
3110 return std::nullopt;
3111 }
3112 ValueRecord const* const record = value.asRecord();
3113 if (record != nullptr && record->size() == 1) {
3114 auto const& [form, amount] = *record->begin();
3115 std::int64_t const* const units = amount.asInt();
3116 if (form == "fixed" && units != nullptr && *units >= 0) {
3117 return *units;
3118 }
3119 if (form == "stretch" && units != nullptr && *units >= 1) {
3120 return std::nullopt;
3121 }
3122 }
3123 refuse(path, std::string{kForms});
3124 }
3125
3126 // A node's size keys: each bound a literal integer of 0 or more, a minimum no more than its maximum, no bound on
3127 // an overlay or a custom node; and a literal `sizing` one of the forms the interpreter reads, with a fixed amount
3128 // inside its dimension's bounds. A sizing that is not literal is read at mount.
3129 static void checkLayoutKeys(DocNode const& node) { checkSizing(node, literalBounds(node)); }
3130
3131 // The size bounds a node gives, `minWidth`, `maxWidth`, `minHeight` and `maxHeight` in that order: literal
3132 // integers of 0 or more, no minimum above its maximum, none on an overlay or a custom node.
3133 static std::array<std::optional<std::int64_t>, 4> literalBounds(DocNode const& node) {
3134 static constexpr std::array<std::string_view, 4> kBounds = {"minWidth", "maxWidth", "minHeight", "maxHeight"};
3135 std::array<std::optional<std::int64_t>, 4> bounds{};
3136 for (std::size_t index = 0; index < kBounds.size(); ++index) {
3137 std::string_view const key = kBounds.at(index);
3138 ExprPtr const bound = node.prop(key);
3139 if (bound == nullptr) {
3140 continue;
3141 }
3142 if (node.kind == "dialog" || node.kind == "drawer") {
3143 refuse(bound->path, "an overlay is sized by the frontend, not by its container");
3144 }
3145 if (node.kind == "custom") {
3146 refuse(bound->path,
3147 "a custom node carries no common properties, so it takes no `" + std::string{key} + "`");
3148 }
3149 std::int64_t const* const units =
3150 bound->kind == ExprKind::Literal ? bound->literal.asInt() : static_cast<std::int64_t const*>(nullptr);
3151 if (units == nullptr || *units < 0) {
3152 refuse(bound->path, "`" + std::string{key} + "` is a literal integer of 0 or more, in backend units");
3153 }
3154 bounds.at(index) = *units;
3155 }
3156 for (std::size_t axis = 0; axis < 2; ++axis) {
3157 auto const& minimum = bounds.at(2 * axis);
3158 auto const& maximum = bounds.at((2 * axis) + 1);
3159 if (minimum && maximum && *minimum > *maximum) {
3160 refuse(pointerAppend(node.path, kBounds.at(2 * axis)),
3161 "`" + std::string{kBounds.at(2 * axis)} + "` is more than `" +
3162 std::string{kBounds.at((2 * axis) + 1)} + "`");
3163 }
3164 }
3165 return bounds;
3166 }
3167
3168 // Whether a literal sizing dimension's value is `{"stretch": w}` (as opposed to `"content"` or `{"fixed": n}`,
3169 // which both `sizingDimension` reports the same way -- as "no fixed amount"). An overlay is sized by the
3170 // frontend, not by its container, so stretching it would silently change how every *other* sibling in its own
3171 // parent layout is justified; checked ahead of `sizingDimension` itself, which does not distinguish the two.
3172 static bool isStretch(Value const& value) {
3173 ValueRecord const* const record = value.asRecord();
3174 return record != nullptr && record->size() == 1 && record->begin()->first == "stretch";
3175 }
3176
3177 // An overlay (`dialog`/`drawer`) refuses a literal `sizing` that stretches either dimension, single-value or
3178 // record form: an overlay is sized by the frontend, not by its container, so stretching it would silently change
3179 // how every *other* sibling in its own parent layout is justified.
3180 static void checkOverlaySizing(DocNode const& node, ExprPtr const& sizing) {
3181 if (node.kind != "dialog" && node.kind != "drawer") {
3182 return;
3183 }
3184 ValueRecord const* const record = sizing->literal.asRecord();
3185 bool const stretches =
3186 isStretch(sizing->literal) ||
3187 (record != nullptr && (record->contains("width") || record->contains("height")) &&
3188 std::ranges::any_of(*record, [](auto const& entry) { return isStretch(entry.second); }));
3189 if (stretches) {
3190 refuse(sizing->path,
3191 "an overlay is sized by the frontend, not by its container; give it `{\"fixed\": n}` instead "
3192 "of `{\"stretch\": w}`");
3193 }
3194 }
3195
3196 // A literal `sizing` is one of its forms, and a fixed amount lies inside its dimension's @p bounds.
3197 static void checkSizing(DocNode const& node, std::array<std::optional<std::int64_t>, 4> const& bounds) {
3198 ExprPtr const sizing = node.prop("sizing");
3199 if (sizing == nullptr || sizing->kind != ExprKind::Literal) {
3200 return;
3201 }
3202 checkOverlaySizing(node, sizing);
3203 std::optional<std::int64_t> width;
3204 std::optional<std::int64_t> height;
3205 ValueRecord const* const record = sizing->literal.asRecord();
3206 if (record != nullptr && (record->contains("width") || record->contains("height"))) {
3207 for (auto const& [dimension, value] : *record) {
3208 if (dimension == "width") {
3209 width = sizingDimension(value, sizing->path);
3210 } else if (dimension == "height") {
3211 height = sizingDimension(value, sizing->path);
3212 } else {
3213 refuse(sizing->path, "a `sizing` record has `width` and `height`; `" + dimension + "` is neither");
3214 }
3215 }
3216 } else {
3217 width = sizingDimension(sizing->literal, sizing->path);
3218 }
3219 for (auto const& [fixed, axis] : {std::pair{width, std::size_t{0}}, std::pair{height, std::size_t{1}}}) {
3220 auto const& minimum = bounds.at(2 * axis);
3221 auto const& maximum = bounds.at((2 * axis) + 1);
3222 if (fixed && (*fixed < minimum.value_or(*fixed) || *fixed > maximum.value_or(*fixed))) {
3223 refuse(sizing->path, std::string{"a fixed size has no room to bound: the "} +
3224 (axis == 0 ? "width" : "height") + " is fixed outside its bounds");
3225 }
3226 }
3227 }
3228
3229 // A text's literal `role` is one of the roles (`title` is `heading`'s alias); a computed one is read at mount, and
3230 // one of no known name is reported there.
3231 static void checkTextRole(DocNode const& node) {
3232 ExprPtr const role = node.prop("role");
3233 if (role == nullptr || role->kind != ExprKind::Literal) {
3234 return;
3235 }
3236 static constexpr std::array<std::string_view, 7> kRoles = {"normal", "muted", "heading", "title",
3237 "section", "error", "success"};
3238 std::string const* const name = role->literal.asString();
3239 if (name == nullptr) {
3240 refuse(pointerAppend(node.path, "role"), "`role` is a name");
3241 }
3242 if (!listed(kRoles, *name)) {
3243 refuse(pointerAppend(node.path, "role"), "`" + *name + "` is not one of the values of `role`");
3244 }
3245 }
3246
3247 // A form's `columns`: a literal integer, 1 to 12, the `x-colspan` convention's full row.
3248 static void checkFormColumns(DocNode const& node) {
3249 ExprPtr const columns = node.prop("columns");
3250 if (columns == nullptr) {
3251 return;
3252 }
3253 std::int64_t const* const count =
3254 columns->kind == ExprKind::Literal ? columns->literal.asInt() : static_cast<std::int64_t const*>(nullptr);
3255 if (count == nullptr) {
3256 refuse(columns->path, "`columns` is a literal integer");
3257 }
3258 if (*count < 1 || *count > 12) {
3259 refuse(columns->path, "`columns` is 1 to 12; a form lays its fields in at least one column");
3260 }
3261 }
3262
3263 static void checkChoice(DocNode const& node, std::string_view key, std::span<std::string_view const> choices) {
3264 std::string_view const value = node.name(key);
3265 if (!value.empty() && !listed(choices, value)) {
3266 refuse(pointerAppend(node.path, key),
3267 "`" + std::string{value} + "` is not one of the values of `" + std::string{key} + "`");
3268 }
3269 }
3270
3271 void checkCommands(Commands const& commands, CheckContext const& context, bool sequence = false) {
3272 CheckContext current = context;
3273 current.commands = true;
3274 for (Command const& cmd : commands) {
3275 checkCommand(cmd, current);
3276 if (sequence && cmd.kind == CommandKind::Run && !cmd.binding.empty() && !cmd.extra) {
3277 current.bindings.emplace_back(cmd.binding, Type::any());
3278 }
3279 }
3280 }
3281
3282 // NOLINTNEXTLINE(readability-function-cognitive-complexity): one case per command of ui/1.
3283 void checkCommand(Command const& cmd, CheckContext const& context) {
3284 std::string const where = cmd.path;
3285 switch (cmd.kind) {
3286 case CommandKind::Set:
3287 checkSet(cmd, context);
3288 break;
3289 case CommandKind::Reset: {
3290 NameEntry const& entry = require(context, cmd.target, where,
3291 {NameCategory::State, NameCategory::Element}, "state or a dialog");
3292 if (entry.category == NameCategory::Element && entry.element->kind != "dialog" &&
3293 entry.element->kind != "drawer") {
3294 refuse(where, "`reset` restores state, or the state of a dialog; `" + cmd.target + "` is a " +
3295 entry.element->kind);
3296 }
3297 break;
3298 }
3299 case CommandKind::Run: {
3300 static_cast<void>(require(context, cmd.target, where, {NameCategory::Mutation}, "a mutation"));
3301 CheckContext body = context;
3302 if (cmd.extra) {
3303 checkExpr(*cmd.extra, context);
3304 if (!cmd.binding.empty()) {
3305 body.bindings.emplace_back(cmd.binding, Type::any());
3306 }
3307 }
3308 if (cmd.value) {
3309 checkExpr(*cmd.value, body);
3310 }
3311 break;
3312 }
3313 case CommandKind::Refetch:
3314 static_cast<void>(require(context, cmd.target, where, {NameCategory::Query}, "a query"));
3315 break;
3316 case CommandKind::Open:
3317 case CommandKind::Close:
3318 if (cmd.kind == CommandKind::Close && cmd.target == "self") {
3319 break;
3320 }
3321 checkDialog(cmd, context);
3322 break;
3323 case CommandKind::Focus:
3324 static_cast<void>(require(context, cmd.target, where, {NameCategory::Element}, "an element"));
3325 break;
3326 case CommandKind::ResetPrefs:
3327 if (cmd.target != "all" && !_doc->customize.contains(cmd.target)) {
3328 refuse(where, "`" + cmd.target + "` is not a customization point of this document");
3329 }
3330 break;
3331 case CommandKind::If:
3332 checkCondition(*cmd.value, context, "the condition of `if`");
3333 checkCommands(cmd.commands, context);
3334 checkCommands(cmd.alternative, context);
3335 break;
3336 case CommandKind::Seq:
3337 checkCommands(cmd.commands, context, true);
3338 break;
3339 case CommandKind::All:
3340 case CommandKind::Navigate:
3341 if (cmd.value) {
3342 checkExpr(*cmd.value, context);
3343 }
3344 if (cmd.extra) {
3345 checkExpr(*cmd.extra, context);
3346 }
3347 checkCommands(cmd.commands, context);
3348 checkCommands(cmd.alternative, context);
3349 break;
3350 case CommandKind::CancelClose:
3351 if (!context.onClose) {
3352 refuse(where, "`cancelClose` is valid only in `onClose`");
3353 }
3354 break;
3355 case CommandKind::Retry:
3356 if (!context.boundaryError) {
3357 refuse(where, "`retry` is valid only inside a boundary's `error`");
3358 }
3359 break;
3360 case CommandKind::SignIn:
3361 static_cast<void>(require(context, cmd.target, where, {NameCategory::Mutation}, "a mutation"));
3362 break;
3363 case CommandKind::Upload:
3364 static_cast<void>(require(context, cmd.target, where, {NameCategory::Mutation}, "a mutation"));
3365 checkExpr(*cmd.value, context);
3366 break;
3367 case CommandKind::Download:
3368 checkExpr(*cmd.value, context);
3369 if (cmd.extra) {
3370 checkExpr(*cmd.extra, context);
3371 }
3372 if (cmd.mediaType) {
3373 checkExpr(*cmd.mediaType, context);
3374 }
3375 if (cmd.option == "preview") {
3376 static_cast<void>(require(context, cmd.target, where, {NameCategory::State}, "state"));
3377 }
3378 break;
3379 case CommandKind::Return:
3380 case CommandKind::Host:
3381 case CommandKind::Notify:
3382 case CommandKind::SwitchBackend:
3383 case CommandKind::SetLocale:
3384 case CommandKind::SignOut:
3385 default:
3386 if (cmd.value) {
3387 checkExpr(*cmd.value, context);
3388 }
3389 break;
3390 }
3391 }
3392
3393 static void checkDialog(Command const& cmd, CheckContext const& context) {
3394 NameEntry const& entry = require(context, cmd.target, cmd.path, {NameCategory::Element}, "a dialog");
3395 if (entry.element->kind != "dialog" && entry.element->kind != "drawer") {
3396 refuse(cmd.path, "`" + cmd.target + "` is a " + entry.element->kind + ", not a dialog or drawer");
3397 }
3398 }
3399
3400 void checkSet(Command const& cmd, CheckContext const& context) {
3401 auto const segments = splitPath(cmd.target);
3402 std::string const& head = segments->front();
3403 Type const value = checkExpr(*cmd.value, context);
3404 if (head == "app") {
3405 return;
3406 }
3407 NameEntry& entry =
3408 require(context, head, cmd.path, {NameCategory::State, NameCategory::Element}, "state or an input");
3409 if (entry.category == NameCategory::Element) {
3410 KindSpec const* const spec = findKind(entry.element->kind, false);
3411 if ((spec->flags & kWritable) == 0) {
3412 refuse(cmd.path, "`set` writes state or an input's draft; `" + head + "` is a " + entry.element->kind);
3413 }
3414 return;
3415 }
3416 auto const type = derivedType(entry, head);
3417 if (type && segments->size() == 1 && !compatible(*type, value)) {
3418 refuse(cmd.value->path, "`" + head + "` is " + type->name() + "; the value is " + value.name());
3419 }
3420 }
3421
3422 /// The state and elements an expression reads, through the `let` entries it reads.
3423 void reads(Expr const& expr, ScopeInfo* scope, std::set<NameEntry const*>& out, std::vector<std::string>& locals,
3424 std::set<NameEntry const*>& seenLets) {
3425 if (expr.kind == ExprKind::Ref && std::ranges::find(locals, expr.name) == locals.end()) {
3426 NameEntry const* const entry = lookup(scope, expr.name);
3427 if (entry != nullptr &&
3428 (entry->category == NameCategory::State || entry->category == NameCategory::Element)) {
3429 out.insert(entry);
3430 } else if (entry != nullptr && entry->category == NameCategory::Let && seenLets.insert(entry).second) {
3431 std::vector<std::string> inner;
3432 reads(*entry->derivedFrom, entry->scope, out, inner, seenLets);
3433 }
3434 }
3435 bool const binds = expr.kind == ExprKind::Map || expr.kind == ExprKind::Filter;
3436 for (std::size_t index = 0; index < expr.args.size(); ++index) {
3437 if (binds && index == 1) {
3438 locals.push_back(expr.name);
3439 }
3440 reads(*expr.args.at(index), scope, out, locals, seenLets);
3441 if (binds && index == 1) {
3442 locals.pop_back();
3443 }
3444 }
3445 for (auto const& [name, field] : expr.fields) {
3446 static_cast<void>(name);
3447 reads(*field, scope, out, locals, seenLets);
3448 }
3449 }
3450
3451 /// The state and elements a command list writes.
3452 static void writes(Commands const& commands, ScopeInfo* scope, std::set<NameEntry const*>& out) {
3453 for (Command const& cmd : commands) {
3454 if (cmd.kind == CommandKind::Set || cmd.kind == CommandKind::Reset) {
3455 auto const segments = splitPath(cmd.target);
3456 NameEntry const* const entry = lookup(scope, segments->front());
3457 if (entry != nullptr) {
3458 out.insert(entry);
3459 }
3460 }
3461 writes(cmd.commands, scope, out);
3462 writes(cmd.alternative, scope, out);
3463 }
3464 }
3465
3466 void checkWatchGraph() {
3467 std::size_t const count = _watches.size();
3468 std::vector<std::set<NameEntry const*>> observed(count);
3469 std::vector<std::set<NameEntry const*>> written(count);
3470 for (std::size_t index = 0; index < count; ++index) {
3471 auto const& [watch, scope] = _watches.at(index);
3472 std::vector<std::string> locals;
3473 std::set<NameEntry const*> seenLets;
3474 reads(*watch->on, scope, observed.at(index), locals, seenLets);
3475 writes(watch->body, scope, written.at(index));
3476 }
3477 // 0 unvisited, 1 on the current path, 2 finished.
3478 std::vector<int> state(count, 0);
3479 std::vector<std::size_t> trail;
3480 std::function<void(std::size_t)> visit = [&](std::size_t from) {
3481 state.at(from) = 1;
3482 trail.push_back(from);
3483 for (std::size_t to = 0; to < count; ++to) {
3484 bool edge = false;
3485 for (NameEntry const* entry : written.at(from)) {
3486 edge = edge || observed.at(to).contains(entry);
3487 }
3488 if (!edge) {
3489 continue;
3490 }
3491 if (state.at(to) == 1) {
3492 std::string cycle;
3493 auto const start = std::ranges::find(trail, to);
3494 for (auto step = start; step != trail.end(); ++step) {
3495 cycle += _watches.at(*step).first->path + " → ";
3496 }
3497 refuse(_watches.at(to).first->path,
3498 "the watch graph has a cycle: " + cycle + _watches.at(to).first->path);
3499 }
3500 if (state.at(to) == 0) {
3501 visit(to);
3502 }
3503 }
3504 trail.pop_back();
3505 state.at(from) = 2;
3506 };
3507 for (std::size_t index = 0; index < count; ++index) {
3508 if (state.at(index) == 0) {
3509 visit(index);
3510 }
3511 }
3512 }
3513
3514 // The effect checks below read only what the catalog says about each action. They warn only where the
3515 // effects certainly cover, or certainly miss, what the document declares, because a warning a document cannot
3516 // act on would teach its author to ignore the rest.
3517
3518 /// The effects as a phrase, such as "`changes T_Row`, `none`".
3519 [[nodiscard]] static std::string describeEffects(std::vector<ActionEffect> const& effects) {
3520 std::string out;
3521 for (ActionEffect const& effect : effects) {
3522 out += (out.empty() ? "`" : ", `") + effect.kind + (effect.record ? " " + *effect.record : "") + "`";
3523 }
3524 return out;
3525 }
3526
3527 /// Whether an action's effects change anything; `none` alone does not.
3528 [[nodiscard]] static bool changesData(ActionInfo const& info) {
3529 return info.effects &&
3530 std::ranges::any_of(*info.effects, [](ActionEffect const& effect) { return effect.kind != "none"; });
3531 }
3532
3533 void warn(LoadWarningKind kind, std::string path, std::string text) {
3534 _warnings.push_back(LoadWarning{.kind = kind, .path = std::move(path), .text = std::move(text)});
3535 }
3536
3537 void checkEffects() {
3538 if (!_options->catalog.action) {
3539 return;
3540 }
3541 for (auto const& entry : _mutations) {
3542 checkMutationEffects(*entry.first);
3543 }
3544 for (auto const& [watch, scope] : _watches) {
3545 checkWatchFeedback(*watch, scope);
3546 }
3547 }
3548
3549 void checkMutationEffects(MutationDecl const& mutation) {
3550 auto const action = _options->catalog.action(mutation.action);
3551 if (!action) {
3552 return;
3553 }
3554 if (!action->effects && !refetches(mutation.onSuccess)) {
3555 warn(LoadWarningKind::Uncovered, mutation.path,
3556 std::format("nothing refreshes after `{}` succeeds: `{}` declares no effects, and no `refetch` "
3557 "follows it; declare what it changes, or `effects::none` when it changes nothing",
3558 mutation.name, mutation.action));
3559 }
3560 }
3561
3562 /// Whether a command list refetches a query, which is a refresh the document declares itself.
3563 [[nodiscard]] static bool refetches(Commands const& commands) {
3564 return std::ranges::any_of(commands, [](Command const& cmd) {
3565 return cmd.kind == CommandKind::Refetch || refetches(cmd.commands) || refetches(cmd.alternative);
3566 });
3567 }
3568
3569 // Another client's change refetches the watched query, the watch runs the mutation, and its change reaches the
3570 // other client, which may do the same. Only `"source": "user"` keeps a refetch from running the watch.
3571 void checkWatchFeedback(WatchDecl const& watch, ScopeInfo* scope) {
3572 if (watch.source == WatchSource::User) {
3573 return;
3574 }
3575 std::vector<std::string> locals;
3576 std::set<NameEntry const*> seenLets;
3577 auto const query = readQuery(*watch.on, scope, locals, seenLets);
3578 if (!query) {
3579 return;
3580 }
3581 if (auto const mutation = runsWriter(watch.body, scope)) {
3582 warn(LoadWarningKind::WatchFeedback, watch.path,
3583 std::format("the watch reads the query `{}` and runs `{}`, which changes data: another client's "
3584 "change refetches the query and runs it again; give the watch \"source\": \"user\"",
3585 *query, *mutation));
3586 }
3587 }
3588
3589 /// The first query whose value an expression reads, directly or through a `let`.
3590 [[nodiscard]] static std::optional<std::string> readQuery(Expr const& expr, ScopeInfo* scope,
3591 std::vector<std::string>& locals,
3592 std::set<NameEntry const*>& seenLets) {
3593 if (expr.kind == ExprKind::Query) {
3594 return expr.name;
3595 }
3596 if (expr.kind == ExprKind::Ref && std::ranges::find(locals, expr.name) == locals.end()) {
3597 NameEntry const* const entry = lookup(scope, expr.name);
3598 if (entry != nullptr && entry->category == NameCategory::Query) {
3599 return expr.name;
3600 }
3601 if (entry != nullptr && entry->category == NameCategory::Let && seenLets.insert(entry).second) {
3602 std::vector<std::string> inner;
3603 if (auto found = readQuery(*entry->derivedFrom, entry->scope, inner, seenLets)) {
3604 return found;
3605 }
3606 }
3607 }
3608 bool const binds = expr.kind == ExprKind::Map || expr.kind == ExprKind::Filter;
3609 for (std::size_t index = 0; index < expr.args.size(); ++index) {
3610 if (binds && index == 1) {
3611 locals.push_back(expr.name);
3612 }
3613 auto found = readQuery(*expr.args.at(index), scope, locals, seenLets);
3614 if (binds && index == 1) {
3615 locals.pop_back();
3616 }
3617 if (found) {
3618 return found;
3619 }
3620 }
3621 for (auto const& [name, field] : expr.fields) {
3622 static_cast<void>(name);
3623 if (auto found = readQuery(*field, scope, locals, seenLets)) {
3624 return found;
3625 }
3626 }
3627 return std::nullopt;
3628 }
3629
3630 /// The first mutation a command list runs whose action changes data.
3631 [[nodiscard]] std::optional<std::string> runsWriter(Commands const& commands, ScopeInfo* scope) const {
3632 for (Command const& cmd : commands) {
3633 bool const runs =
3634 cmd.kind == CommandKind::Run || cmd.kind == CommandKind::Upload || cmd.kind == CommandKind::SignIn;
3635 NameEntry const* const entry = runs ? lookup(scope, cmd.target) : nullptr;
3636 if (entry != nullptr && entry->mutation != nullptr) {
3637 auto const action = _options->catalog.action(entry->mutation->action);
3638 if (action && changesData(*action)) {
3639 return cmd.target;
3640 }
3641 }
3642 if (auto found = runsWriter(cmd.commands, scope)) {
3643 return found;
3644 }
3645 if (auto found = runsWriter(cmd.alternative, scope)) {
3646 return found;
3647 }
3648 }
3649 return std::nullopt;
3650 }
3651
3652 Document const* _doc;
3653 LoadOptions const* _options;
3654 std::deque<ScopeInfo> _scopes;
3655 std::map<DocNode const*, ScopeInfo*> _scopeOf;
3656 std::map<DocNode const*, ScopeInfo*> _innerOf;
3657 std::map<std::string, QueryDecl const*, std::less<>> _queries;
3658 std::map<std::string, DocNode const*, std::less<>> _elements;
3659 std::vector<std::pair<WatchDecl const*, ScopeInfo*>> _watches;
3660 std::vector<std::pair<MutationDecl const*, ScopeInfo*>> _mutations;
3661 std::vector<std::pair<QueryDecl const*, ScopeInfo*>> _queryScopes;
3662 std::vector<LoadWarning> _warnings;
3663 std::map<DocNode const*, TypeKind> _dateValueKinds;
3664 std::vector<std::string> _deriving;
3665 // NOLINTEND(misc-no-recursion)
3666};
3667
3668// NOLINTNEXTLINE(readability-function-cognitive-complexity): one case per kind of name.
3669inline std::expected<Type, std::string> ScopeResolver::ref(std::string_view head,
3670 std::span<std::string const> path) const {
3671 for (auto const& [name, type] : std::views::reverse(_context.bindings)) {
3672 if (name == head) {
3673 return typeAlong(type, head, path);
3674 }
3675 }
3676 if (head == "event") {
3677 if (!_context.commands) {
3678 return std::unexpected(std::string{"`event` is read only inside a command list"});
3679 }
3680 return Type::any();
3681 }
3682 if (head == "app") {
3683 return Type::any();
3684 }
3685 if (head == "boundary") {
3686 if (!_context.boundaryError || path.size() != 1 || path.front() != "error") {
3687 return std::unexpected(std::string{"`boundary.error` is read only inside a boundary's `error`"});
3688 }
3689 return Type::of(TypeKind::String, true);
3690 }
3691 NameEntry* const entry = DocumentValidator::lookup(_context.scope, head);
3692 if (entry == nullptr) {
3693 return std::unexpected("`" + std::string{head} + "` does not resolve in this scope");
3694 }
3695 switch (entry->category) {
3697 return typeAlong(entry->type, head, path);
3699 case NameCategory::Let: {
3700 auto type = _validator.derivedType(*entry, head);
3701 if (!type) {
3702 return type;
3703 }
3704 return typeAlong(*type, head, path);
3705 }
3707 return Type::any();
3709 return std::unexpected("`" + std::string{head} +
3710 "` is a mutation; read it with `result`, `pending` or "
3711 "`error`");
3713 return std::unexpected("`" + std::string{head} + "` is a model alias, not a value");
3715 if (path.empty() || !listed(kTransferValues, path.front())) {
3716 return std::unexpected("the transfer `" + std::string{head} +
3717 "` is read as `progress`, `pending` or `error`");
3718 }
3719 return Type::any();
3721 default: {
3722 KindSpec const* const spec = findKind(entry->element->kind, false);
3723 if (path.empty()) {
3724 return std::unexpected("the element `" + std::string{head} +
3725 "` is read through one of its values, "
3726 "such as `" +
3727 std::string{head} + ".visible`");
3728 }
3729 std::string const& value = path.front();
3730 bool const open = (spec->flags & kOpenValues) != 0;
3731 bool const known = listed(kAnyValues, value) || listed(spec->values, value) || open;
3732 if (!known) {
3733 return std::unexpected("the " + entry->element->kind + " `" + std::string{head} + "` has no value `" +
3734 value + "`");
3735 }
3736 bool const field = open && !listed(kAnyValues, value) && !listed(spec->values, value);
3737 Type const type = field ? Type::any() : elementValueType(value);
3738 return typeAlong(type, value, path.subspan(1));
3739 }
3740 }
3741}
3742
3743inline std::expected<Type, std::string> ScopeResolver::row(std::span<std::string const> path) const {
3744 static_cast<void>(path);
3745 for (ScopeInfo const* scope = _context.scope; scope != nullptr; scope = scope->parent) {
3746 if (scope->row) {
3747 return Type::any();
3748 }
3749 }
3750 return std::unexpected(std::string{"`row` is read only inside a forEach or table row"});
3751}
3752
3753inline std::expected<Type, std::string> ScopeResolver::control(ExprKind form, std::string_view name) const {
3754 if (form == ExprKind::Can && name.starts_with("host:")) {
3755 return Type::of(TypeKind::Bool);
3756 }
3757 NameEntry const* const entry = DocumentValidator::lookup(_context.scope, name);
3758 bool const query = entry != nullptr && entry->category == NameCategory::Query;
3759 bool const mutation = entry != nullptr && entry->category == NameCategory::Mutation;
3760 if (form == ExprKind::Query && !query) {
3761 return std::unexpected("`" + std::string{name} + "` is not a query of this scope");
3762 }
3763 if (form == ExprKind::Result && !mutation) {
3764 return std::unexpected("`" + std::string{name} + "` is not a mutation of this scope");
3765 }
3766 if (!query && !mutation) {
3767 return std::unexpected("`" + std::string{name} + "` is not a query or mutation of this scope");
3768 }
3769 if (form == ExprKind::Pending || form == ExprKind::Can) {
3770 return Type::of(TypeKind::Bool);
3771 }
3772 if (form == ExprKind::Error || form == ExprKind::ErrorKind) {
3773 return Type::of(TypeKind::String, true);
3774 }
3775 return Type::any();
3776}
3777
3778inline std::expected<Type, std::string> ScopeResolver::pref(std::string_view point,
3779 std::span<std::string const> path) const {
3780 auto const& points = _validator.document().customize;
3781 auto const found = points.find(point);
3782 if (found == points.end()) {
3783 return std::unexpected("`" + std::string{point} + "` is not a customization point of this document");
3784 }
3785 return typeAlong(found->second.type, point, path);
3786}
3787
3788} // namespace detail
3789
3790/// @brief Parses a UI document and runs its load-time validation (spec 5 §3).
3791///
3792/// A document is refused when it does not parse, when a reference does not resolve, when a type does not check,
3793/// when the `let` or `watch` graph has a cycle, when an expression exceeds 256 nodes, when a node kind, command or
3794/// function is not in the vocabulary, when a document declares `invalidates`, `patch` or `refreshOn`, when a
3795/// must-understand key is unknown, or when a rule of spec 5 or spec 6 fails; the list is in
3796/// `docs/spec/ui/document.md`. The rules that need catalog facts run when @p options supplies them.
3797/// @param json The document's JSON.
3798/// @param options The catalog facts.
3799/// @return The document, with what validation found that does not refuse it in `Document::warnings`, or the first
3800/// refusal, naming the document id, the JSON Pointer and the reason.
3801[[nodiscard]] inline std::expected<Document, Refusal> loadDocument(Json const& json, LoadOptions const& options = {}) {
3802 std::string documentId;
3803 if (auto const* const object = std::get_if<Json::object_t>(&json.data)) {
3804 auto const* const found = object->find("id");
3805 if (found != object->end()) {
3806 if (auto const* const text = std::get_if<std::string>(&found->second.data)) {
3807 documentId = *text;
3808 }
3809 }
3810 }
3811 try {
3813 detail::DocumentValidator validator{doc, options};
3814 validator.run();
3815 doc.warnings = validator.takeWarnings();
3816 doc.dateValueKinds = validator.takeDateValueKinds();
3817 return doc;
3818 } catch (detail::DocumentRefused const& refused) {
3819 Refusal refusal = refused.refusal();
3820 refusal.document = documentId;
3821 return std::unexpected(std::move(refusal));
3822 }
3823}
3824
3825/// @brief Parses JSON text as a UI document and runs its load-time validation.
3826/// @param text The document's JSON text.
3827/// @param options The catalog facts.
3828/// @return The document, or the refusal; text that is not JSON is refused at the root.
3829[[nodiscard]] inline std::expected<Document, Refusal> loadDocumentText(std::string_view text,
3830 LoadOptions const& options = {}) {
3831 auto json = parseJson(text);
3832 if (!json) {
3833 return std::unexpected(Refusal{.document = {}, .path = "", .reason = std::move(json.error())});
3834 }
3835 return loadDocument(*json, options);
3836}
3837
3838/// @brief The view node kinds of `ui/1`.
3839/// @return Their `kind` spellings.
3840[[nodiscard]] inline std::vector<std::string_view> nodeKinds() {
3841 std::vector<std::string_view> kinds;
3842 for (detail::KindSpec const& spec : detail::kKinds) {
3843 if ((spec.flags & detail::kPart) == 0) {
3844 kinds.push_back(spec.kind);
3845 }
3846 }
3847 return kinds;
3848}
3849
3850} // namespace morph::ui
AnyHandle list(std::vector< AnyHandle > const &items)
{"list": [...]}: a list built from expressions.
Definition author.hpp:1123
Command set(AnyHandle const &target, AnyHandle const &value)
set: writes a state, an input's value or an app name.
Definition author.hpp:2724
Node button(ButtonOptions const &options)
A button node.
Definition author.hpp:3156
bool names(DocNode const &node, std::string_view key)
Whether a node has a property that names it: present, and not the constant "" or null.
Definition author.hpp:4373
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Definition author.hpp:1000
Node form(ModelHandle< Model > const &model, FormOptions< Action > const &options={})
A form node: a schema form of the forms engine over an action (spec 2); each field takes its label fr...
Definition author.hpp:4271
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
Node id(Element const &element) const
The element id, by which relations read its values.
Definition author.hpp:1818
Turns the JSON of a document into its model, refusing what does not parse.
Document parse(Json const &json)
Parses the whole document.
The exception the loader throws inside itself; loadDocument turns it into its result.
Definition document.hpp:582
Refusal const & refusal() const noexcept
The refusal.
Definition document.hpp:590
DocumentRefused(Refusal refusal)
Wraps a refusal.
Definition document.hpp:586
Checks a parsed document against its own names and the catalog: every load-time rule that needs more ...
std::vector< LoadWarning > takeWarnings() noexcept
What validation found that does not refuse the document.
std::map< DocNode const *, TypeKind > takeDateValueKinds() noexcept
The static type of each date input's value that the checker could decide.
DocumentValidator(Document const &doc, LoadOptions const &options)
Validates doc.
Document const & document() const noexcept
The document.
static NameEntry * lookup(ScopeInfo *scope, std::string_view name)
Finds a name in a scope chain.
std::expected< Type, std::string > derivedType(NameEntry &entry, std::string_view name)
The type of a name whose type is derived from an expression, derived on first use.
void run()
Runs every rule; throws DocumentRefused on the first refusal.
std::expected< Type, std::string > row(std::span< std::string const > path) const override
Resolves a row field.
std::expected< Type, std::string > control(ExprKind form, std::string_view name) const override
Resolves a query or mutation name.
std::expected< Type, std::string > pref(std::string_view point, std::span< std::string const > path) const override
Resolves a customization point.
ScopeResolver(DocumentValidator &validator, CheckContext const &context)
Resolves for validator in context.
std::expected< Type, std::string > ref(std::string_view head, std::span< std::string const > path) const override
Resolves a reference.
constexpr std::array< KeySpec, 2 > kScrollKeys
The keys of scroll.
Definition document.hpp:775
constexpr std::array< KeySpec, 2 > kBusyKeys
The keys of busy.
Definition document.hpp:797
Concurrency
What a mutation run does while another is in flight.
Definition document.hpp:217
@ Exclusive
The run is refused.
Definition document.hpp:218
constexpr std::array< std::string_view, 3 > kFormValues
The element values of a form, beside its fields.
Definition document.hpp:710
constexpr std::array< KeySpec, 7 > kCollectionKeys
The keys of collection and wizard: the model and action, and the presentation keys that reach every f...
Definition document.hpp:916
constexpr std::array< KeySpec, 9 > kDialogKeys
The keys of dialog and drawer.
Definition document.hpp:787
constexpr std::array< KindSpec, 48 > kKinds
The view node kinds of ui/1 and the part kinds inside them.
Definition document.hpp:934
constexpr std::array< KeySpec, 2 > kTextKeys
The keys of text.
Definition document.hpp:715
constexpr std::array< KeySpec, 1 > kTabsKeys
The keys of tabs.
Definition document.hpp:783
constexpr std::array< KeySpec, 2 > kTabKeys
The keys of a tabs page part.
Definition document.hpp:785
constexpr std::array< KeySpec, 2 > kDropKeys
The keys of a drop part.
Definition document.hpp:931
InstanceMode
How a model alias binds its instance (spec 5 §5).
Definition document.hpp:146
@ Borrowed
The opener's binding of an alias ({"from": alias}), for a screen opened as a dialog.
Definition document.hpp:151
@ Keyed
The shared instance an expression's key selects.
Definition document.hpp:150
@ Default
None named: shared when the catalog marks the model stateless, private otherwise.
Definition document.hpp:147
constexpr std::array< KeySpec, 7 > kButtonKeys
The keys of button.
Definition document.hpp:717
constexpr std::array< KeySpec, 4 > kDropZoneKeys
The keys of dropZone.
Definition document.hpp:891
constexpr std::array< KeySpec, 1 > kMenuKeys
The keys of menu.
Definition document.hpp:748
std::vector< std::string_view > nodeKinds()
The view node kinds of ui/1.
constexpr std::array< KeySpec, 7 > kMenuItemKeys
The keys of a menu item part.
Definition document.hpp:750
NameCategory
What a name in a scope declares.
@ Transfer
A transfer id of upload.
constexpr std::array< KeySpec, 6 > kDateTimeKeys
The keys of dateTimeInput.
Definition document.hpp:830
constexpr std::array< KeySpec, 5 > kCollapsibleKeys
The keys of collapsible.
Definition document.hpp:885
constexpr std::array< KeySpec, 10 > kTextInputKeys
The keys of textInput.
Definition document.hpp:725
constexpr std::uint8_t kBoundaryError
The key's content is a boundary's error, where boundary.error and retry are valid.
Definition document.hpp:625
constexpr std::array< KeySpec, 4 > kEmptyStateKeys
The keys of emptyState.
Definition document.hpp:879
constexpr std::array< KeySpec, 7 > kSliderKeys
The keys of slider.
Definition document.hpp:837
constexpr std::string_view kVocabulary
The vocabulary a document declares; the only one this client reads.
Definition document.hpp:37
constexpr std::array< KeySpec, 2 > kA11yKeys
The keys of an a11y part.
Definition document.hpp:927
constexpr std::array< std::string_view, 10 > kTableValues
The element values of a table.
Definition document.hpp:704
void refuseMustUnderstand(Json::object_t const &object, std::string const &path)
Refuses a must-understand key (!name) the client does not know; other unknown keys are ignored.
constexpr std::array< KeySpec, 5 > kFilePickerKeys
The keys of filePicker.
Definition document.hpp:845
constexpr std::array< KeySpec, 1 > kCustomizeKeys
The keys of customize.
Definition document.hpp:894
Shape
What shape a node key's value takes.
Definition document.hpp:604
@ Nodes
A node or a list of nodes.
Definition document.hpp:608
@ Name
A plain string that is a name.
Definition document.hpp:610
@ Part
A record, parsed by the part table of its part kind.
Definition document.hpp:611
@ Expr
An expression.
Definition document.hpp:605
@ CommandRecord
A record of command lists.
Definition document.hpp:615
@ Parts
A list of such records.
Definition document.hpp:612
@ Text
A plain string.
Definition document.hpp:609
@ Condition
An expression checked as a bool.
Definition document.hpp:606
@ Keys
Key bindings: chord to command list.
Definition document.hpp:616
@ ExprRecord
A record of expressions.
Definition document.hpp:614
@ Commands
A command list.
Definition document.hpp:607
@ Flag
A literal true or false, set once.
Definition document.hpp:617
@ PartsOrExpr
A list of records, or an expression yielding one.
Definition document.hpp:613
constexpr std::array< KeySpec, 4 > kFieldKeys
The input decorations text and the inputs take.
Definition document.hpp:688
bool compatible(Type const &declared, Type const &actual)
Whether a value of type actual may stand where declared is expected.
constexpr std::array< KeySpec, 3 > kCustomKeys
The keys of custom.
Definition document.hpp:924
constexpr std::array< KeySpec, 3 > kBoundaryKeys
The keys of boundary.
Definition document.hpp:896
constexpr std::array< KeySpec, 5 > kBannerKeys
The keys of banner.
Definition document.hpp:851
std::function< std::optional< std::string >(std::string_view)> documentLookup(std::map< std::string, std::string, std::less<> > documents)
A Catalog::view or Catalog::wizard lookup over documents by action id.
Definition document.hpp:127
constexpr std::array< KeySpec, 2 > kStepsKeys
The keys of steps.
Definition document.hpp:875
constexpr std::array< KeySpec, 3 > kSplitterKeys
The keys of splitter.
Definition document.hpp:882
std::vector< Command > Commands
A command list, run as one batch.
Definition document.hpp:239
constexpr std::uint8_t kScope
The node or part creates a scope.
Definition document.hpp:640
constexpr std::array< KeySpec, 1 > kKeyValueKeys
The keys of keyValue.
Definition document.hpp:877
Type elementValueType(std::string_view value)
The type of an element value.
std::expected< Document, Refusal > loadDocument(Json const &json, LoadOptions const &options={})
Parses a UI document and runs its load-time validation (spec 5 §3).
constexpr std::array< KeySpec, 15 > kFormKeys
The keys of form.
Definition document.hpp:899
WatchSource
Which changes a watch reacts to.
Definition document.hpp:333
@ Any
Every change.
Definition document.hpp:334
constexpr std::array< std::string_view, 2 > kSelectValues
The element values of a select.
Definition document.hpp:702
constexpr std::array< KeySpec, 4 > kHeadBadgeKeys
The keys of a headBadge part: a status badge of a screen's head.
Definition document.hpp:867
void checkChord(std::string const &chord, std::string const &path)
Parses a key chord, and refuses one the workspace shell reserves.
constexpr std::array< std::string_view, 3 > kDialogValues
The element values of a dialog or drawer.
Definition document.hpp:708
constexpr std::array< KeySpec, 3 > kBadgeKeys
The keys of badge.
Definition document.hpp:870
constexpr std::array< KeySpec, 8 > kSelectKeys
The keys of select.
Definition document.hpp:739
KindSpec const * findKind(std::string_view kind, bool part)
Looks up a node kind or a part kind.
Definition document.hpp:990
constexpr std::array< KeySpec, 5 > kColumnKeys
The keys of a table column part.
Definition document.hpp:824
std::expected< Document, Refusal > loadDocumentText(std::string_view text, LoadOptions const &options={})
Parses JSON text as a UI document and runs its load-time validation.
constexpr std::array< KeySpec, 4 > kCheckboxKeys
The keys of checkbox.
Definition document.hpp:736
constexpr std::array< KeySpec, 4 > kContainerKeys
The keys of column and row.
Definition document.hpp:758
bool listed(std::span< std::string_view const > names, std::string_view name)
Whether a list of names holds one.
constexpr std::array< KeySpec, 2 > kCaseKeys
The keys of a switch case part.
Definition document.hpp:780
constexpr std::array< std::string_view, 3 > kTransferValues
The values of a transfer id.
Definition document.hpp:712
constexpr std::uint8_t kWritable
A command may write its draft with set.
Definition document.hpp:648
std::shared_ptr< DocNode const > DocNodePtr
A shared, immutable view node.
Definition document.hpp:387
constexpr std::array< KeySpec, 2 > kProgressKeys
The keys of progress.
Definition document.hpp:873
constexpr std::array< KeySpec, 18 > kTableKeys
The keys of table.
Definition document.hpp:805
constexpr std::array< std::string_view, 5 > kInputValues
The element values of a text, date-time or slider input.
Definition document.hpp:698
CommandKind
The commands of ui/1 (spec 5 §8, spec 6).
Definition document.hpp:242
@ ResetPrefs
{"resetPrefs": "point" | "all"}
Definition document.hpp:254
@ SignIn
{"signIn": {"from": "m"}}
Definition document.hpp:262
@ Navigate
{"navigate": "screen" | {"route": e}, "params": e, "as": ..., "onResult": [...], ....
Definition document.hpp:249
@ Set
{"set": "name.path", "value": e}
Definition document.hpp:243
@ Retry
{"retry": true}
Definition document.hpp:261
@ If
{"if": [cond, [...], [...]]}
Definition document.hpp:259
@ Return
{"return": e}
Definition document.hpp:250
@ Refetch
{"refetch": "q"}
Definition document.hpp:246
@ Notify
{"notify": e, "role": "success"}
Definition document.hpp:256
@ SignOut
{"signOut": true}
Definition document.hpp:263
@ Seq
{"seq": [...]}
Definition document.hpp:257
@ Host
{"host": "name", "args": e?}
Definition document.hpp:252
@ Open
{"open": "dialog"}
Definition document.hpp:247
@ Run
{"run": "m", "body": e?, "as": "x"?, "each": e?}
Definition document.hpp:245
@ CancelClose
{"cancelClose": true}
Definition document.hpp:260
@ SwitchBackend
{"switchBackend": "name"}
Definition document.hpp:253
@ Close
{"close": "dialog" | "self"}
Definition document.hpp:248
@ Focus
{"focus": "id"}
Definition document.hpp:251
@ SetLocale
{"setLocale": "de"}
Definition document.hpp:255
constexpr std::array< KeySpec, 1 > kDragKeys
The keys of a drag part.
Definition document.hpp:929
constexpr std::array< std::string_view, 5 > kAnyValues
The element values every element has.
Definition document.hpp:696
constexpr std::uint8_t kOpenValues
Any path below its id is an element value (a form's fields).
Definition document.hpp:650
constexpr std::array< KeySpec, 5 > kForEachKeys
The keys of forEach.
Definition document.hpp:799
constexpr std::uint8_t kInner
The key is evaluated in the node's own scope (a dialog's, a row's), not the scope around it.
Definition document.hpp:621
constexpr std::array< std::string_view, 1 > kCheckboxValues
The element values of a checkbox.
Definition document.hpp:700
LoadWarningKind
What a load-time warning is about. A warning never refuses a document.
Definition document.hpp:508
@ Uncovered
A mutation whose action declares nothing, and after which nothing refreshes.
Definition document.hpp:510
@ WatchFeedback
A watch of a query runs a mutation that changes data, on any change of it.
Definition document.hpp:509
constexpr std::array< KeySpec, 7 > kActionButtonKeys
The keys of an actionButton part: a button a screen's head or an overlay's footer shows.
Definition document.hpp:859
void refuse(std::string const &path, std::string reason)
Refuses the document.
Definition document.hpp:599
constexpr std::uint8_t kRequired
The key is required.
Definition document.hpp:623
constexpr std::array< KeySpec, 3 > kSwitchKeys
The keys of switch.
Definition document.hpp:777
constexpr std::uint8_t kPart
It is a part, not a node: it has no common properties.
Definition document.hpp:646
constexpr std::array< KeySpec, 2 > kActionKeys
The keys of an action part.
Definition document.hpp:857
constexpr std::uint8_t kRowScope
The scope it creates is a row scope, where row is valid.
Definition document.hpp:642
constexpr std::array< KeySpec, 5 > kGridKeys
The keys of grid; justify is read only to refuse it.
Definition document.hpp:761
constexpr std::uint8_t kField
It carries the input decorations readonly, required, errors and stale.
Definition document.hpp:644
constexpr std::array< KeySpec, 7 > kPanelKeys
The keys of panel.
Definition document.hpp:767
constexpr std::array< KeySpec, 18 > kCommonKeys
The keys every node takes.
Definition document.hpp:666
The expr/1 expression language of the UI document: parsing a JSON expression into an Expr tree,...
std::expected< ExprPtr, Refusal > parseExpression(Json const &json, std::string const &path="")
Parses one expression.
std::expected< Type, std::string > typeAlong(Type const &type, std::string_view head, std::span< std::string const > path)
The type a field path reads from a value of a known type.
std::expected< Type, Refusal > checkExpression(Expr const &expr, NameResolver const &resolver)
Checks an expression against the names a document declares.
std::expected< std::vector< std::string >, std::string > splitPath(std::string_view text)
Splits head.a.b into its segments.
Value evaluate(Expr const &expr, Environment const &env)
Evaluates an expression.
constexpr bool isName(std::string_view text) noexcept
Whether a string is a document name: [A-Za-z_][A-Za-z0-9_]*.
ExprKind
The forms of an expr/1 expression (spec 5 §4).
@ ErrorKind
{"errorKind": "m"}: the kind of a query's or mutation's error.
@ Pending
{"pending": "q"}: whether a query or mutation is in flight.
@ Query
{"query": "q"}: a query's value.
@ Result
{"result": "m"}: a mutation's last result.
@ Error
{"error": "q"}: a query's or mutation's error text.
@ Can
{"can": "m"}: whether the principal may run a mutation or query.
std::shared_ptr< Expr const > ExprPtr
A shared, immutable expression node.
std::expected< PageRows, TableError > apply(DataSource const &source, TableQuery const &query, ApplyOptions const &options={})
Runs query over source in the calling thread: the reference implementation of server mode.
Definition query.hpp:289
One effect an action declares, as the catalog states it.
Definition document.hpp:48
friend bool operator==(ActionEffect const &lhs, ActionEffect const &rhs)=default
Memberwise equality.
std::string kind
What the effect is: updates, changes, changesInstance or none.
Definition document.hpp:50
std::optional< std::string > record
The wire name of the record the effect names; absent for changesInstance and none.
Definition document.hpp:52
What the catalog says about an action.
Definition document.hpp:78
std::optional< std::vector< ActionEffect > > effects
What the action declares it changes. Absent when it declared nothing, which makes it a read; present ...
Definition document.hpp:94
std::vector< std::string > secretFields
The reply fields the server marks secret.
Definition document.hpp:85
std::vector< std::string > calculatedFields
The fields of the action's reply rows that the server calculates; an optimistic write may not name th...
Definition document.hpp:83
std::string modelType
The registered type id of the model the action belongs to.
Definition document.hpp:80
std::vector< ActionRecordInfo > records
The records the action's result holds, with the paths to each. Empty when it holds none or the catalo...
Definition document.hpp:97
bool credential
Whether the action sends a credential.
Definition document.hpp:87
std::optional< std::vector< std::string > > listFields
When the action's result is a record, the names of its members that hold a list: what a patch or opti...
Definition document.hpp:91
A record an action's result holds, as the catalog states it.
Definition document.hpp:62
std::string record
The record's wire name.
Definition document.hpp:64
std::optional< std::string > keyField
The record's key field; absent for a keyless record.
Definition document.hpp:66
friend bool operator==(ActionRecordInfo const &lhs, ActionRecordInfo const &rhs)=default
Memberwise equality.
std::vector< std::string > paths
The JSON paths at which the result holds the record; "" is the result itself.
Definition document.hpp:68
The facts load-time validation needs from the application's catalog. Local mode answers them from the...
Definition document.hpp:105
std::function< std::optional< std::string >(std::string_view actionId)> schema
The schema JSON of an action, as forms::schemaJson<A>() emits it: what a form node builds its form fr...
Definition document.hpp:113
std::function< std::optional< std::string >(std::string_view actionId)> wizard
The w-* wizard document whose first step is an action, as flows::wizardSchemaJson<W>() emits it: what...
Definition document.hpp:121
std::function< std::optional< ModelInfo >(std::string_view typeId)> model
Looks up a model type by its registered type id.
Definition document.hpp:107
std::function< std::optional< std::string >(std::string_view actionId)> view
The v-* view document whose v-query is an action, as views::viewSchemaJson<V>() emits it: what a coll...
Definition document.hpp:117
std::function< std::optional< ActionInfo >(std::string_view actionId)> action
Looks up an action by its id.
Definition document.hpp:109
A validation across elements (spec 5 §6).
Definition document.hpp:351
std::vector< std::string > targets
The elements the message attaches to.
Definition document.hpp:361
ExprPtr message
The message added to each target's errors.
Definition document.hpp:359
std::string id
The check's id.
Definition document.hpp:353
ExprPtr assertion
The condition; false fails the check, null does not.
Definition document.hpp:357
std::string path
The declaration's JSON Pointer.
Definition document.hpp:355
bool blocking
Whether a failure blocks ready.
Definition document.hpp:363
One command. Which members a kind uses is listed with each member.
Definition document.hpp:271
std::string option
A closed option: navigate's as, notify's role, download's save or to.
Definition document.hpp:289
std::string transfer
The transfer id of upload, whose progress is an element value.
Definition document.hpp:291
Commands commands
Nested commands: seq and all; if's then branch; navigate's onResult.
Definition document.hpp:301
ExprPtr value
The main expression: set's value, run's body, navigate's params, return's value, host's args,...
Definition document.hpp:283
std::string nameAs
upload's body members for an inline file's name and media type; empty for none.
Definition document.hpp:297
bool bytes
Whether the file moves inline: download's "from": "bytes", upload's "into": "bytes".
Definition document.hpp:293
std::string typeAs
See nameAs.
Definition document.hpp:299
CommandKind kind
The kind.
Definition document.hpp:273
ExprPtr extra
The second expression: run's each, navigate's route, download's suggested name.
Definition document.hpp:285
std::string path
The command's JSON Pointer.
Definition document.hpp:275
std::string target
The name it acts on: set's name.path; the state of reset; the mutation of run, signIn and upload; the...
Definition document.hpp:280
std::string binding
A binding name: run's and upload's as.
Definition document.hpp:287
ExprPtr mediaType
download's media type of inline bytes.
Definition document.hpp:295
Commands alternative
The alternative: if's else branch; navigate's onCancel.
Definition document.hpp:303
A custom component declaration (spec 5 §10).
Definition document.hpp:470
std::string host
The name of a host component; empty for a delivered one.
Definition document.hpp:478
std::map< std::string, Type, std::less<> > events
The events and their payload types.
Definition document.hpp:484
std::int64_t version
The host component's version.
Definition document.hpp:480
std::map< std::string, std::string, std::less<> > props
The props and their prop types (string, bool, int, exact, number, json).
Definition document.hpp:482
std::string qml
The content hash of a delivered QML component (sha256:<hex>); empty for a host component.
Definition document.hpp:476
std::string name
The component's name.
Definition document.hpp:472
std::string path
The declaration's JSON Pointer.
Definition document.hpp:474
DocNodePtr fallback
The subtree of built-in nodes a renderer shows when it cannot load the component.
Definition document.hpp:486
A customization point (spec 5 §12b).
Definition document.hpp:490
Type type
The type of a value.
Definition document.hpp:502
std::vector< std::string > options
The options of a choice.
Definition document.hpp:500
std::string name
The point's name.
Definition document.hpp:492
Value defaultValue
The default choice; null when none is declared.
Definition document.hpp:504
std::string of
The element a columns or arrange point adjusts.
Definition document.hpp:498
std::string kind
columns, choice, value, layout, saved, arrange, and in the app document pins.
Definition document.hpp:496
std::string path
The declaration's JSON Pointer.
Definition document.hpp:494
A view node of the document (spec 5 §9), or a structured part of one (a table column,...
Definition document.hpp:394
std::map< std::string, Commands, std::less<> > keys
Key bindings: chord to commands.
Definition document.hpp:413
std::map< std::string, std::vector< DocNodePtr >, std::less<> > children
Child node lists, by key (children, header, loading, error, default, cell, fallback).
Definition document.hpp:406
std::string path
The node's JSON Pointer.
Definition document.hpp:400
std::optional< ScopeDecl > scope
The declarations of the scope the node creates (dialog, drawer, switch case, forEach or table row); e...
Definition document.hpp:418
std::string_view name(std::string_view key) const
A plain string property.
Definition document.hpp:455
Commands const * event(std::string_view key) const
A command list.
Definition document.hpp:431
std::map< std::string, std::vector< DocNodePtr >, std::less<> > parts
Structured parts, by key (columns, tabs, cases, items, a11y, action, fields, …).
Definition document.hpp:408
std::string kind
The node kind (text, table, …), or for a part its part kind (column, tab, case, …).
Definition document.hpp:396
std::map< std::string, Commands, std::less<> > events
Command lists, by key (onClick, …), including a custom node's on events as on.<event>.
Definition document.hpp:404
std::map< std::string, bool, std::less<> > flags
Literal flags, by key (showLabel of an input).
Definition document.hpp:415
bool flag(std::string_view key) const
A literal flag.
Definition document.hpp:463
std::span< DocNodePtr const > partList(std::string_view key) const
A list of parts.
Definition document.hpp:447
std::map< std::string, std::string, std::less<> > names
Plain string properties, by key (testId, surface, name of a custom node, model and action of a form,...
Definition document.hpp:411
ExprPtr prop(std::string_view key) const
An expression property.
Definition document.hpp:423
std::string id
The element id; empty when none is given.
Definition document.hpp:398
std::span< DocNodePtr const > nodes(std::string_view key) const
A child node list.
Definition document.hpp:439
std::map< std::string, ExprPtr, std::less<> > props
Expression properties, by key.
Definition document.hpp:402
A loaded and validated UI document: a screen, or the app document (id app, spec 6 §5).
Definition document.hpp:536
std::string id
The document id, such as lims.results.
Definition document.hpp:540
std::map< std::string, Commands, std::less<> > lifecycle
The lifecycle command lists by key: onMount, onUnmount, onClose, onSave, onBackendChange.
Definition document.hpp:558
std::map< std::string, Commands, std::less<> > keys
The screen's key bindings.
Definition document.hpp:560
std::map< std::string, Component, std::less<> > components
Custom components, by name.
Definition document.hpp:566
std::map< DocNode const *, TypeKind > dateValueKinds
The static type of each dateTimeInput's value that the loader could decide: String or Timestamp,...
Definition document.hpp:573
std::string vocab
The vocabulary, ui/1.
Definition document.hpp:538
ExprPtr dirty
The unsaved-changes expression.
Definition document.hpp:546
DocNodePtr view
The view; nullptr for the app document.
Definition document.hpp:570
std::vector< LoadWarning > warnings
What load-time validation found that does not refuse the document, in the order found....
Definition document.hpp:576
ScopeDecl scope
The screen scope's declarations.
Definition document.hpp:556
ExprPtr subtitle
The subtitle expression of the screen's head; nullptr when none is declared.
Definition document.hpp:548
std::vector< DocNodePtr > status
The head's status badges: headBadge parts, in order.
Definition document.hpp:550
std::vector< std::string > prefetch
Queries started before the screen mounts.
Definition document.hpp:564
std::string showErrors
When errors show: touched, submit or always; empty for the default.
Definition document.hpp:562
ExprPtr title
The title expression.
Definition document.hpp:542
std::vector< std::pair< std::string, Type > > params
The typed params, in declaration order.
Definition document.hpp:554
std::map< std::string, CustomizePoint, std::less<> > customize
Customization points, by name.
Definition document.hpp:568
std::vector< DocNodePtr > actions
The head's actions: actionButton parts, in order.
Definition document.hpp:552
ExprPtr identity
The identity expression: equal identities are one screen.
Definition document.hpp:544
One node of a parsed expression. Immutable once parsed; shared between the document and its readers.
A derived value (let).
Definition document.hpp:183
std::string path
The declaration's JSON Pointer.
Definition document.hpp:187
ExprPtr value
The expression.
Definition document.hpp:189
std::string name
The name.
Definition document.hpp:185
Options of loadDocument.
Definition document.hpp:140
Catalog catalog
The catalog facts.
Definition document.hpp:142
A finding of load-time validation that does not refuse the document.
Definition document.hpp:514
LoadWarningKind kind
What it is about.
Definition document.hpp:516
std::string text
The finding, in one sentence.
Definition document.hpp:520
std::string path
The JSON Pointer of the declaration it names.
Definition document.hpp:518
std::string message(std::string_view document) const
The warning as one line of text.
Definition document.hpp:525
A model alias.
Definition document.hpp:155
InstanceMode instance
How the instance binds.
Definition document.hpp:163
std::string path
The declaration's JSON Pointer.
Definition document.hpp:159
std::string name
The alias.
Definition document.hpp:157
std::string borrowedFrom
The opener's alias of a Borrowed instance.
Definition document.hpp:167
std::string type
The model's registered type id.
Definition document.hpp:161
ExprPtr key
The key expression of a Keyed instance.
Definition document.hpp:165
What the catalog says about a model type.
Definition document.hpp:40
bool keyed
Whether the model can be shared by an instance key, so an alias may give instance an expression.
Definition document.hpp:44
bool stateless
Whether a shared singleton is equivalent to a private instance.
Definition document.hpp:42
A mutation (spec 5 §5).
Definition document.hpp:307
Concurrency concurrency
What a run does while another is in flight.
Definition document.hpp:319
ExprPtr body
The body; nullptr sends an empty object.
Definition document.hpp:317
std::optional< OptimisticDecl > optimistic
The row a run predicts before its reply.
Definition document.hpp:323
Commands onError
Commands run after a failure.
Definition document.hpp:327
std::string model
The model alias it goes through.
Definition document.hpp:313
std::string action
The action id it runs.
Definition document.hpp:315
ExprPtr serialKey
The queue key of serial runs.
Definition document.hpp:321
Commands onSuccess
Commands run after a success.
Definition document.hpp:325
bool offlineQueue
Whether a run that cannot reach the server is queued offline.
Definition document.hpp:329
std::string name
The name.
Definition document.hpp:309
std::string path
The declaration's JSON Pointer.
Definition document.hpp:311
An optimistic write: the row a mutation run predicts before its reply.
Definition document.hpp:225
std::string query
The query whose cached list it writes.
Definition document.hpp:227
ExprPtr with
The predicted row, read against the run's body bound as body.
Definition document.hpp:233
std::string at
The member of the query's record value that holds the list; empty when the value is the list itself.
Definition document.hpp:229
ExprPtr key
The row key to write, read against the run's body bound as body.
Definition document.hpp:231
A query (spec 5 §5).
Definition document.hpp:193
std::string name
The name.
Definition document.hpp:195
std::string model
The model alias it goes through.
Definition document.hpp:199
std::int64_t staleTime
Milliseconds a cached value counts as fresh.
Definition document.hpp:213
ExprPtr body
The body, which is the query's key; nullptr sends an empty object.
Definition document.hpp:205
bool evaluate
Whether it is an evaluate of the action (spec 5 §7) rather than a call.
Definition document.hpp:203
std::string path
The declaration's JSON Pointer.
Definition document.hpp:197
std::int64_t debounce
Milliseconds the key must be stable before a fetch.
Definition document.hpp:209
std::int64_t refreshEvery
Milliseconds between polls; 0 does not poll.
Definition document.hpp:211
ExprPtr when
The gate; nullptr is always on.
Definition document.hpp:207
std::string action
The action id it runs, or evaluates.
Definition document.hpp:201
Why a document, an expression or a value was refused: where, and the reason.
Definition value.hpp:38
std::string document
The id of the document refused; empty when the refusal is not about a whole document.
Definition value.hpp:40
The declarations of one scope, in declaration order.
Definition document.hpp:367
std::vector< ModelDecl > models
Model aliases.
Definition document.hpp:369
std::vector< StateDecl > state
State.
Definition document.hpp:371
std::vector< LetDecl > lets
Derived values.
Definition document.hpp:373
std::vector< MutationDecl > mutations
Mutations.
Definition document.hpp:377
std::vector< QueryDecl > queries
Queries.
Definition document.hpp:375
std::vector< CheckDecl > checks
Checks.
Definition document.hpp:381
std::vector< WatchDecl > watches
Watches.
Definition document.hpp:379
A state declaration.
Definition document.hpp:171
std::string name
The name.
Definition document.hpp:173
ExprPtr init
The initial value; nullptr starts null.
Definition document.hpp:179
std::string path
The declaration's JSON Pointer.
Definition document.hpp:175
Type type
The declared type; any when the document gives none.
Definition document.hpp:177
Static or bound text.
Definition view.hpp:406
A document type (spec 5 §3): a kind, whether null is allowed, and a list's element type.
Definition value.hpp:311
static Type any()
The type the document cannot know.
Definition value.hpp:321
static Type of(TypeKind kind, bool optional=false)
A scalar or record type.
Definition value.hpp:327
TypeKind kind
The kind.
Definition value.hpp:313
static Type listOf(Type element, bool optional=false)
A list type.
Definition value.hpp:335
bool isAny() const noexcept
Whether the type is any.
Definition value.hpp:342
A cascade: commands that run when a value changes (spec 5 §6).
Definition document.hpp:339
ExprPtr on
The value watched.
Definition document.hpp:343
Commands body
The commands.
Definition document.hpp:347
WatchSource source
Which changes run it.
Definition document.hpp:345
std::string path
The declaration's JSON Pointer.
Definition document.hpp:341
Where an expression or command is checked: its scope and what is valid there.
std::vector< std::pair< std::string, Type > > bindings
Names bound here: as of a sequence step or each, result, body, props.
bool boundaryError
Inside a boundary's error, where boundary.error and retry are valid.
bool onClose
Inside onClose, where cancelClose is valid.
ScopeInfo * scope
The scope.
bool commands
Inside a command list, where event is valid.
One key of a kind.
Definition document.hpp:628
std::string_view key
The key.
Definition document.hpp:630
std::string_view part
The part kind, for Part, Parts and PartsOrExpr.
Definition document.hpp:634
std::uint8_t flags
kInner, kRequired, kBoundaryError.
Definition document.hpp:636
A node kind or part kind: its keys and its element values.
Definition document.hpp:653
std::span< KeySpec const > keys
Its own keys.
Definition document.hpp:657
std::string_view kind
The kind.
Definition document.hpp:655
std::uint8_t flags
kScope, kRowScope, kField, kPart, kWritable, kOpenValues.
Definition document.hpp:659
std::span< std::string_view const > values
The element values beyond those every element has.
Definition document.hpp:661
One declared name of a scope.
ModelDecl const * model
The model of a Model entry.
ScopeInfo * scope
The scope that declares it.
DocNode const * element
The element of an Element entry.
int visit
0 before its type is derived, 1 while it is, 2 after.
ExprPtr derivedFrom
The expression a state or let type is derived from; nullptr when the type is declared.
MutationDecl const * mutation
The mutation of a Mutation entry.
Type type
The type of a param, of typed state, and of state or a let once derived.
NameCategory category
What it declares.
std::string path
The declaration's JSON Pointer.
QueryDecl const * query
The query of a Query entry.
One scope during validation: its names and its parent.
bool row
Whether it is a forEach or table row scope.
std::map< std::string, NameEntry, std::less<> > names
The names it declares.
ScopeInfo * parent
The enclosing scope; nullptr for a screen or a component fallback.
bool screen
Whether it is the screen's own scope.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
@ Record
record: a record whose fields the document does not declare.
Definition value.hpp:303
@ Key
key: an int or a string.
Definition value.hpp:302
@ Decimal
decimal.
Definition value.hpp:299
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
Definition value.hpp:35
std::expected< Type, std::string > parseType(std::string_view text)
Parses a document type: bool, int, string, decimal, quantity, timestamp, key, record,...
Definition value.hpp:479
std::expected< Value, std::string > valueFromJson(Json const &json)
Reads a value from its JSON form, structurally.
Definition value.hpp:570
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Definition value.hpp:129
std::string pointerAppend(std::string_view base, std::string_view token)
Appends one reference token to a JSON Pointer, escaping ~ and / as RFC 6901 requires.
Definition value.hpp:68
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194
Node row(Row spec)
Children laid out left to right.
Definition view.hpp:1229
SlotBinding< std::decay_t< F > > slot(SlotId slotId, F &&read)
A slot with an id.
Definition view.hpp:63