morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
bundle.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <cstddef>
6#include <cstdint>
7#include <expected>
8#include <functional>
9#include <glaze/glaze.hpp>
10#include <initializer_list>
11#include <map>
12#include <memory>
13#include <optional>
14#include <set>
15#include <string>
16#include <string_view>
17#include <tuple>
18#include <utility>
19#include <variant>
20#include <vector>
21
22#include "../core/wire.hpp"
24#include "document.hpp"
25#include "value.hpp"
26
27/// @file
28/// @brief What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest, and
29/// the bodies of the `ui-hello`, `ui-catalog` and `ui-fetch` envelopes, with their bounds and their parsers.
30///
31/// Specified in `docs/spec/ui/service.md`.
32
33namespace morph::ui {
34
35/// @brief The expression vocabulary every bundle needs alongside `ui/1`.
36inline constexpr std::string_view kExprVocabulary = "expr/1";
37
38/// @brief The envelope kind of the handshake.
39inline constexpr std::string_view kHelloKind = "ui-hello";
40/// @brief The envelope kind of the catalog.
41inline constexpr std::string_view kCatalogKind = "ui-catalog";
42/// @brief The envelope kind that fetches a file by its hash.
43inline constexpr std::string_view kFetchKind = "ui-fetch";
44/// @brief The envelope kind that evaluates an action body without running it (spec 5 §7).
45inline constexpr std::string_view kEvaluateKind = "ui-evaluate";
46/// @brief The envelope kind of the file side channel (`files/1`, spec 6 §18): uploads and downloads in chunks.
47inline constexpr std::string_view kFileKind = "ui-file";
48
49/// @brief The pseudo-model the service's envelopes are authorised under: `authorize(session, "ui", <action>)`.
50inline constexpr std::string_view kServiceModel = "ui";
51/// @brief The pseudo-action that gates `ui-hello`, and the fetch of a public file or the manifest.
52inline constexpr std::string_view kHelloAction = "hello";
53/// @brief The pseudo-action that gates `ui-catalog`.
54inline constexpr std::string_view kCatalogAction = "catalog";
55/// @brief The pseudo-action that gates the fetch of a file that is not public.
56inline constexpr std::string_view kFetchAction = "fetch";
57
58/// @brief The prefix of a content hash.
59inline constexpr std::string_view kHashPrefix = "sha256:";
60/// @brief The hexadecimal digits of a SHA-256 digest.
61inline constexpr std::size_t kHashDigits = 64;
62
63/// @brief The most file bytes one `ui-file` chunk carries, before base64url.
64inline constexpr std::size_t kMaxFileChunkBytes = std::size_t{256} * 1024;
65/// @brief The largest `ui-file` request body a server reads: a chunk in base64url, and the rest of the request.
66inline constexpr std::size_t kMaxFileRequestBytes = (kMaxFileChunkBytes / 3 * 4) + (std::size_t{4} * 1024);
67
68/// @brief The largest request body (`ui-hello`, `ui-fetch`) a server reads.
69inline constexpr std::size_t kMaxRequestBytes = std::size_t{64} * 1024;
70/// @brief The largest file a bundle may hold, and a client accepts.
71inline constexpr std::size_t kMaxFileBytes = std::size_t{4} * 1024 * 1024;
72/// @brief The most files a manifest, and entries a catalog, may list.
73inline constexpr std::size_t kMaxBundleFiles = 4096;
74/// @brief The most action ids, model facts or action facts a catalog may carry.
75inline constexpr std::size_t kMaxCatalogFacts = 16384;
76/// @brief The most items of a list or map in a `ui-hello` (vocabularies, host components, commands, values).
77inline constexpr std::size_t kMaxListItems = 256;
78/// @brief The longest name: an id, a kind, a vocabulary, a host name, an application id or version.
79inline constexpr std::size_t kMaxNameBytes = 256;
80
81/// @brief The file kinds of a manifest.
82inline constexpr std::string_view kScreenFile = "screen";
83/// @brief The app document (spec 6 §5).
84inline constexpr std::string_view kAppFile = "app";
85/// @brief A theme (`ui-theme/1`, spec 6 §6).
86inline constexpr std::string_view kThemeFile = "theme";
87/// @brief A message catalogue.
88inline constexpr std::string_view kMessagesFile = "messages";
89/// @brief A delivered custom component (spec 5 §10).
90inline constexpr std::string_view kComponentFile = "component";
91/// @brief An icon of a theme's icon set: SVG text.
92inline constexpr std::string_view kIconFile = "icon";
93/// @brief The largest icon file a bundle may hold.
94inline constexpr std::size_t kMaxIconBytes = std::size_t{64} * 1024;
95
96/// @brief The content hash of @p bytes: `sha256:` and the SHA-256 digest in 64 lower-case hexadecimal digits.
97/// @param bytes The bytes.
98/// @return The hash.
99[[nodiscard]] inline std::string contentHash(std::string_view bytes) {
100 static constexpr std::string_view kHex = "0123456789abcdef";
101 std::string const raw = ::morph::session::detail::sha256(bytes);
102 std::string out{kHashPrefix};
103 out.reserve(kHashPrefix.size() + kHashDigits);
104 for (char const byte : raw) {
105 auto const value = static_cast<unsigned char>(byte);
106 out.push_back(kHex.at(static_cast<std::size_t>(value >> 4U)));
107 out.push_back(kHex.at(static_cast<std::size_t>(value & 0x0FU)));
108 }
109 return out;
110}
111
112/// @brief Whether @p text is a well-formed content hash.
113/// @param text The text.
114/// @return `true` for `sha256:` followed by exactly 64 lower-case hexadecimal digits.
115[[nodiscard]] inline bool isContentHash(std::string_view text) {
116 if (text.size() != kHashPrefix.size() + kHashDigits || !text.starts_with(kHashPrefix)) {
117 return false;
118 }
119 return std::ranges::all_of(text.substr(kHashPrefix.size()), [](char const digit) {
120 return (digit >= '0' && digit <= '9') || (digit >= 'a' && digit <= 'f');
121 });
122}
123
124/// @brief One file of a manifest.
126 /// @brief The file's id: a screen's document id, `app`, a theme's, a catalogue's or a component's name.
127 std::string id;
128 /// @brief `screen`, `app`, `theme`, `messages`, `component` or `icon`; a client fetches no kind it does not know.
129 std::string kind;
130 /// @brief The file's content hash.
131 std::string hash;
132 /// @brief The file's size in bytes.
133 std::uint64_t size = 0;
134 /// @brief Whether anyone the `ui-hello` gate admits may fetch it, before sign-in (spec 6 §8 `public`).
135 bool isPublic = false;
136
137 /// @brief Field-wise equality.
138 /// @return Whether every field is equal.
139 bool operator==(ManifestFile const&) const = default;
140};
141
142/// @brief A bundle's manifest: every file the application serves, by its hash (spec 5 §12, "Content addressing").
143struct Manifest {
144 /// @brief The application's id.
145 std::string application;
146 /// @brief The application's version.
147 std::string version;
148 /// @brief The files, sorted by kind then id.
149 std::vector<ManifestFile> files;
150
151 /// @brief Finds a file.
152 /// @param kind The file kind.
153 /// @param fileId The file id.
154 /// @return The file, or null.
155 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): a manifest is keyed by kind, then id.
156 [[nodiscard]] ManifestFile const* find(std::string_view kind, std::string_view fileId) const {
157 auto const found = std::ranges::find_if(
158 files, [kind, fileId](ManifestFile const& file) { return file.kind == kind && file.id == fileId; });
159 return found == files.end() ? nullptr : &*found;
160 }
161};
162
163/// @brief The body of a `ui-hello` request: what the client speaks and carries.
165 /// @brief The vocabularies the client speaks, such as `ui/1`, `expr/1`, `forms/1`, `ui-theme/1`, `files/1`.
166 std::vector<std::string> vocabularies;
167 /// @brief The client's Qt version; empty for a client without Qt.
168 std::string qt;
169 /// @brief Whether the client loads delivered custom components.
170 bool components = false;
171 /// @brief The host components the client has, by name, with their versions.
172 std::map<std::string, std::int64_t, std::less<>> hostComponents;
173 /// @brief The host commands the client has, by name, with their versions.
174 std::map<std::string, std::int64_t, std::less<>> hostCommands;
175 /// @brief The host values the client has, by name, with their versions.
176 std::map<std::string, std::int64_t, std::less<>> hostValues;
177 /// @brief Whether the client offers to take server-sent `event` envelopes on this connection.
178 bool events = false;
179};
180
181/// @brief The body of a `ui-hello` reply.
183 /// @brief The application's id.
184 std::string application;
185 /// @brief The application's version.
186 std::string version;
187 /// @brief The vocabularies the bundle needs; a client that lacks one is refused.
188 std::vector<std::string> vocabularies;
189 /// @brief The manifest's content hash.
190 std::string digest;
191 /// @brief Whether the server accepted the client's offer of events: it may send `event` envelopes on this
192 /// connection.
193 bool events = false;
194};
195
196/// @brief The body of a `ui-evaluate` reply: what `ActionDispatcher::evaluate` answered.
198 /// @brief The action body, reconciled, with its computed fields filled.
200 /// @brief The field errors, by field name.
201 std::map<std::string, std::string, std::less<>> errors;
202 /// @brief Whether an `execute` of this body would reach the model.
203 bool ready = false;
204};
205
206/// @brief The body of a `ui-file` request: one step of an upload or a download, named by `op`.
208 /// @brief `begin`, `chunk`, `end` or `read`.
209 std::string op;
210 /// @brief `begin`: the file's name.
211 std::string name;
212 /// @brief `begin`: its media type.
213 std::string type;
214 /// @brief `begin`: its size in bytes.
215 std::size_t size = 0;
216 /// @brief `begin`: the model type of the mutation that will reference the file.
217 std::string model;
218 /// @brief `begin`: that mutation's action.
219 std::string action;
220 /// @brief `chunk`, `end`: the upload id `begin` answered.
221 std::string upload;
222 /// @brief `chunk`, `read`: the first byte.
223 std::size_t offset = 0;
224 /// @brief `chunk`: the bytes, base64url without padding.
225 std::string data;
226 /// @brief `read`: the file's reference.
227 std::string ref;
228 /// @brief `read`: at most this many bytes; capped at `kMaxFileChunkBytes`.
230};
231
232/// @brief The body of a `ui-file` reply: the members its `op` answers, the others absent.
233struct FileReply {
234 /// @brief `begin`: the upload id.
235 std::optional<std::string> upload;
236 /// @brief `chunk`: the bytes received so far.
237 std::optional<std::size_t> received;
238 /// @brief `end`: the file's reference.
239 std::optional<std::string> ref;
240 /// @brief `read`: the bytes, base64url without padding.
241 std::optional<std::string> data;
242 /// @brief `read`: the whole file's size.
243 std::optional<std::size_t> total;
244 /// @brief `read`: the file's name.
245 std::optional<std::string> name;
246 /// @brief `read`: its media type.
247 std::optional<std::string> type;
248};
249
250/// @brief One catalog entry (spec 6 §8).
252 /// @brief The screen's id.
253 std::string id;
254 /// @brief The title: a string, or a message reference such as `{"t": "lims.sample.title", "fallback": "…"}`.
256 /// @brief The navigator's module.
257 std::string module;
258 /// @brief The navigator's group.
259 std::string group;
260 /// @brief The icon name.
261 std::string icon;
262 /// @brief Whether the screen opens at most once.
263 bool singleton = false;
264 /// @brief Whether the entry can be opened; `false` lists it disabled with `reason`.
265 bool available = true;
266 /// @brief Why the entry is not available: the application's reason, or `denied`.
267 std::optional<std::string> reason;
268 /// @brief What a permission refusal does to the entry: `hide` or `disable`.
269 std::string whenDenied = "hide";
270 /// @brief Whether the client may fetch the screen before sign-in.
271 bool isPublic = false;
272 /// @brief The host screen the application registered with the client instead of a document.
273 std::optional<std::string> host;
274 /// @brief Whether opening the entry replaces an empty, unmodified tab of it.
275 bool reuseEmpty = false;
276};
277
278/// @brief The body of a `ui-catalog` reply: the catalog filtered for the principal, and the facts a client's
279/// load-time validation reads (`ui::Catalog`).
281 /// @brief The entries the principal sees, in registration order.
282 std::vector<CatalogEntry> entries;
283 /// @brief The action ids the principal may run, which `{"can": …}` reads.
284 std::vector<std::string> permitted;
285 /// @brief The model facts by type id; absent when the server has none to give.
286 std::optional<std::map<std::string, ModelInfo, std::less<>>> models;
287 /// @brief The action facts by action id; absent when the server has none to give.
288 std::optional<std::map<std::string, ActionInfo, std::less<>>> actions;
289 /// @brief The `v-*` view documents a `collection` node builds from, by the action of their `v-query`; absent
290 /// when the server has none to give.
291 std::optional<std::map<std::string, std::string, std::less<>>> views;
292 /// @brief The `w-*` wizard documents a `wizard` node builds from, by the action of their first step; absent when
293 /// the server has none to give.
294 std::optional<std::map<std::string, std::string, std::less<>>> wizards;
295
296 /// @brief The facts as the lookups load-time validation takes. A lookup the reply carried no facts for is left
297 /// empty, so its rules are skipped.
298 /// @return The catalog.
299 [[nodiscard]] Catalog facts() const {
300 Catalog catalog;
301 if (models) {
302 catalog.model = [known = std::make_shared<std::map<std::string, ModelInfo, std::less<>> const>(*models)](
303 std::string_view typeId) -> std::optional<ModelInfo> {
304 auto const found = known->find(typeId);
305 if (found == known->end()) {
306 return std::nullopt;
307 }
308 return found->second;
309 };
310 }
311 if (actions) {
312 catalog.action = [known = std::make_shared<std::map<std::string, ActionInfo, std::less<>> const>(
313 *actions)](std::string_view actionId) -> std::optional<ActionInfo> {
314 auto const found = known->find(actionId);
315 if (found == known->end()) {
316 return std::nullopt;
317 }
318 return found->second;
319 };
320 }
321 if (views) {
322 catalog.view = documentLookup(*views);
323 }
324 if (wizards) {
325 catalog.wizard = documentLookup(*wizards);
326 }
327 return catalog;
328 }
329};
330
331/// @brief The body of a `ui-fetch` request.
333 /// @brief The content hash of the file wanted: the manifest's digest, or a file the manifest lists.
334 std::string hash;
335};
336
337} // namespace morph::ui
338
339/// @brief The wire form of a manifest file: `isPublic` travels as `public`.
340template <>
341struct glz::meta<morph::ui::ManifestFile> {
342 /// @brief The type.
343 using T = morph::ui::ManifestFile;
344 /// @brief Its members, by wire key.
345 static constexpr auto value =
346 glz::object("id", &T::id, "kind", &T::kind, "hash", &T::hash, "size", &T::size, "public", &T::isPublic);
347};
348
349/// @brief The wire form of a catalog entry: `isPublic` travels as `public`.
350template <>
351struct glz::meta<morph::ui::CatalogEntry> {
352 /// @brief The type.
353 using T = morph::ui::CatalogEntry;
354 /// @brief Its members, by wire key.
355 static constexpr auto value =
356 glz::object("id", &T::id, "title", &T::title, "module", &T::module, "group", &T::group, "icon", &T::icon,
357 "singleton", &T::singleton, "available", &T::available, "reason", &T::reason, "whenDenied",
358 &T::whenDenied, "public", &T::isPublic, "host", &T::host, "reuseEmpty", &T::reuseEmpty);
359};
360
361namespace morph::ui {
362
363namespace detail {
364
365/// @brief The must-understand key (`!name`) of one object, if it has one.
366/// @param json The value; anything but an object has none.
367/// @param where What the object is, for the reason.
368/// @return Why the message is refused, or nothing.
369[[nodiscard]] inline std::optional<std::string> mustUnderstand(Json const& json, std::string_view where) {
370 auto const* const object = std::get_if<Json::object_t>(&json.data);
371 if (object == nullptr) {
372 return std::nullopt;
373 }
374 for (auto const& [key, value] : *object) {
375 static_cast<void>(value);
376 if (key.starts_with('!')) {
377 return "the must-understand key `" + key + "` in " + std::string{where} + " is not understood";
378 }
379 }
380 return std::nullopt;
381}
382
383/// @brief The must-understand keys of a message: its top level, every element of its listed arrays and every value
384/// of its listed maps.
385/// @param root The parsed message.
386/// @param what What the message is.
387/// @param arrays The keys of arrays whose elements are objects this layer defines.
388/// @param maps The keys of maps whose values are objects this layer defines.
389/// @return Why the message is refused, or nothing.
390// NOLINTBEGIN(bugprone-easily-swappable-parameters): the arrays' keys, then the maps', as a message lists them.
391[[nodiscard]] inline std::optional<std::string> mustUnderstandIn(Json const& root, std::string_view what,
392 std::initializer_list<std::string_view> arrays,
393 std::initializer_list<std::string_view> maps) {
394 if (auto reason = mustUnderstand(root, what)) {
395 return reason;
396 }
397 auto const* const object = std::get_if<Json::object_t>(&root.data);
398 if (object == nullptr) {
399 return std::nullopt;
400 }
401 for (std::string_view const key : arrays) {
402 auto const* const found = object->find(key);
403 auto const* const items = found == object->end() ? nullptr : std::get_if<Json::array_t>(&found->second.data);
404 if (items == nullptr) {
405 continue;
406 }
407 for (Json const& item : *items) {
408 if (auto reason = mustUnderstand(item, std::string{what} + " " + std::string{key})) {
409 return reason;
410 }
411 }
412 }
413 for (std::string_view const key : maps) {
414 auto const* const found = object->find(key);
415 auto const* const items = found == object->end() ? nullptr : std::get_if<Json::object_t>(&found->second.data);
416 if (items == nullptr) {
417 continue;
418 }
419 for (auto const& [name, item] : *items) {
420 if (auto reason = mustUnderstand(item, std::string{what} + " " + std::string{key} + " " + name)) {
421 return reason;
422 }
423 }
424 }
425 return std::nullopt;
426}
427// NOLINTEND(bugprone-easily-swappable-parameters)
428
429/// @brief Reads a message: bounded depth, must-understand keys refused, unknown keys ignored.
430/// @tparam T The message type.
431/// @param text The JSON text.
432/// @param what What the message is, for the reason.
433/// @param arrays The keys of arrays whose elements are objects this layer defines.
434/// @param maps The keys of maps whose values are objects this layer defines.
435/// @return The message, or why it was refused.
436template <typename T>
437[[nodiscard]] std::expected<T, std::string> readMessage(std::string_view text, std::string_view what,
438 std::initializer_list<std::string_view> arrays,
439 std::initializer_list<std::string_view> maps) {
440 auto const tree = parseJson(text);
441 if (!tree) {
442 return std::unexpected(std::string{what} + ": " + tree.error());
443 }
444 if (auto reason = mustUnderstandIn(*tree, what, arrays, maps)) {
445 return std::unexpected(std::move(*reason));
446 }
447 T value{};
448 if (auto const error = glz::read<glz::opts{.error_on_unknown_keys = false}>(value, text)) {
449 return std::unexpected(std::string{what} + ": " + glz::format_error(error, text));
450 }
451 return value;
452}
453
454/// @brief Writes a message as JSON, escaping control bytes as `wire::encode` does.
455/// @tparam T The message type.
456/// @param value The message.
457/// @return The JSON text.
458template <typename T>
459[[nodiscard]] std::string writeMessage(T const& value) {
460 std::string out;
461 // A reflected struct of strings, numbers, maps and generic JSON has no write error to report.
462 static_cast<void>(glz::write<::morph::wire::detail::EscapingWriteOpts{}>(value, out));
463 return out;
464}
465
466/// @brief Checks a name's length.
467/// @param name The name.
468/// @param what What it names, for the reason.
469/// @return Why it is refused, or nothing.
470// NOLINTNEXTLINE(bugprone-easily-swappable-parameters): the name, then what it names, as the reason reads.
471[[nodiscard]] inline std::optional<std::string> checkName(std::string_view name, std::string_view what) {
472 if (name.size() > kMaxNameBytes) {
473 return std::string{what} + " is longer than " + std::to_string(kMaxNameBytes) + " bytes";
474 }
475 return std::nullopt;
476}
477
478/// @brief Checks a list of names: its length and each name's.
479/// @param names The names.
480/// @param limit The most names allowed.
481/// @param list What the list is, for the reason.
482/// @param item What one name is, for the reason.
483/// @return Why the list is refused, or nothing.
484// NOLINTBEGIN(bugprone-easily-swappable-parameters): the list's description, then one item's, as the reasons read.
485[[nodiscard]] inline std::optional<std::string> checkNames(std::vector<std::string> const& names, std::size_t limit,
486 std::string_view list, std::string_view item) {
487 if (names.size() > limit) {
488 return std::string{list} + " list more than " + std::to_string(limit) + " items";
489 }
490 for (std::string const& name : names) {
491 if (auto reason = checkName(name, item)) {
492 return reason;
493 }
494 }
495 return std::nullopt;
496}
497
498/// @brief Checks a map of host names to versions: its size and each name's length.
499/// @param names The map.
500/// @param list What the map holds, for the reason.
501/// @param item What one name is, for the reason.
502/// @return Why the map is refused, or nothing.
503[[nodiscard]] inline std::optional<std::string> checkHostNames(
504 std::map<std::string, std::int64_t, std::less<>> const& names, std::string_view list, std::string_view item) {
505 if (names.size() > kMaxListItems) {
506 return std::string{list} + " list more than " + std::to_string(kMaxListItems) + " items";
507 }
508 for (auto const& [name, version] : names) {
509 static_cast<void>(version);
510 if (auto reason = checkName(name, item)) {
511 return reason;
512 }
513 }
514 return std::nullopt;
515}
516// NOLINTEND(bugprone-easily-swappable-parameters)
517
518} // namespace detail
519
520/// @brief The manifest's text: the JSON its digest is the hash of. Equal manifests write equal text.
521/// @param manifest The manifest; its files should be sorted by kind then id.
522/// @return The JSON text.
523[[nodiscard]] inline std::string manifestText(Manifest const& manifest) { return detail::writeMessage(manifest); }
524
525/// @brief Reads a manifest a server sent.
526///
527/// Refused: text that is not JSON or nests deeper than `kMaxJsonDepth`, a must-understand key at the top or in a
528/// file, more than `kMaxBundleFiles` files, a name longer than `kMaxNameBytes`, a malformed hash, a size above
529/// `kMaxFileBytes`, or two files of one kind with one id. Unknown keys are ignored.
530/// @param text The manifest's text, already verified against its digest.
531/// @return The manifest, or why it was refused.
532[[nodiscard]] inline std::expected<Manifest, std::string> parseManifest(std::string_view text) {
533 auto manifest = detail::readMessage<Manifest>(text, "the manifest", {"files"}, {});
534 if (!manifest) {
535 return std::unexpected(std::move(manifest.error()));
536 }
537 if (manifest->files.size() > kMaxBundleFiles) {
538 return std::unexpected("the manifest lists more than " + std::to_string(kMaxBundleFiles) + " files");
539 }
540 if (auto reason = detail::checkName(manifest->application, "the application id")) {
541 return std::unexpected(std::move(*reason));
542 }
543 if (auto reason = detail::checkName(manifest->version, "the application version")) {
544 return std::unexpected(std::move(*reason));
545 }
546 std::set<std::pair<std::string_view, std::string_view>> seen;
547 for (ManifestFile const& file : manifest->files) {
548 if (auto reason = detail::checkName(file.id, "a file id")) {
549 return std::unexpected(std::move(*reason));
550 }
551 if (auto reason = detail::checkName(file.kind, "a file kind")) {
552 return std::unexpected(std::move(*reason));
553 }
554 if (!isContentHash(file.hash)) {
555 return std::unexpected("the manifest file `" + file.id + "` has a malformed hash");
556 }
557 if (file.size > kMaxFileBytes) {
558 return std::unexpected("the manifest file `" + file.id + "` is larger than " +
559 std::to_string(kMaxFileBytes) + " bytes");
560 }
561 if (!seen.emplace(file.kind, file.id).second) {
562 return std::unexpected("the manifest lists the " + file.kind + " `" + file.id + "` twice");
563 }
564 }
565 return manifest;
566}
567
568/// @brief Reads a `ui-hello` request body.
569///
570/// Refused: a body longer than `kMaxRequestBytes`, text that is not JSON or nests too deep, a must-understand key,
571/// more than `kMaxListItems` vocabularies or host names of a kind, or a name longer than `kMaxNameBytes`.
572/// @param text The request body; empty reads as a client that speaks nothing.
573/// @return The request, or why it was refused.
574[[nodiscard]] inline std::expected<HelloRequest, std::string> parseHelloRequest(std::string_view text) {
575 if (text.size() > kMaxRequestBytes) {
576 return std::unexpected("the ui-hello request is larger than " + std::to_string(kMaxRequestBytes) + " bytes");
577 }
578 auto request = detail::readMessage<HelloRequest>(text.empty() ? std::string_view("{}") : text,
579 "the ui-hello request", {}, {});
580 if (!request) {
581 return std::unexpected(std::move(request.error()));
582 }
583 if (auto reason = detail::checkNames(request->vocabularies, kMaxListItems, "the vocabularies", "a vocabulary")) {
584 return std::unexpected(std::move(*reason));
585 }
586 if (auto reason = detail::checkName(request->qt, "the Qt version")) {
587 return std::unexpected(std::move(*reason));
588 }
589 if (auto reason = detail::checkHostNames(request->hostComponents, "the host components", "a host component")) {
590 return std::unexpected(std::move(*reason));
591 }
592 if (auto reason = detail::checkHostNames(request->hostCommands, "the host commands", "a host command")) {
593 return std::unexpected(std::move(*reason));
594 }
595 if (auto reason = detail::checkHostNames(request->hostValues, "the host values", "a host value")) {
596 return std::unexpected(std::move(*reason));
597 }
598 return request;
599}
600
601/// @brief Reads a `ui-hello` reply body.
602///
603/// Refused: text that is not JSON or nests too deep, a must-understand key, more than `kMaxListItems` vocabularies,
604/// a name longer than `kMaxNameBytes`, or a digest that is not a content hash.
605/// @param text The reply body.
606/// @return The reply, or why it was refused.
607[[nodiscard]] inline std::expected<HelloReply, std::string> parseHelloReply(std::string_view text) {
608 auto reply = detail::readMessage<HelloReply>(text, "the ui-hello reply", {}, {});
609 if (!reply) {
610 return std::unexpected(std::move(reply.error()));
611 }
612 if (auto reason = detail::checkNames(reply->vocabularies, kMaxListItems, "the vocabularies", "a vocabulary")) {
613 return std::unexpected(std::move(*reason));
614 }
615 if (auto reason = detail::checkName(reply->application, "the application id")) {
616 return std::unexpected(std::move(*reason));
617 }
618 if (auto reason = detail::checkName(reply->version, "the application version")) {
619 return std::unexpected(std::move(*reason));
620 }
621 if (!isContentHash(reply->digest)) {
622 return std::unexpected("the ui-hello reply's digest is not a content hash");
623 }
624 return reply;
625}
626
627/// @brief Reads a `ui-evaluate` reply body.
628///
629/// Refused: text that is not JSON or nests too deep, a must-understand key at the top, a body that is not an object,
630/// or more than `kMaxCatalogFacts` field errors.
631/// @param text The reply body.
632/// @return The reply, or why it was refused.
633[[nodiscard]] inline std::expected<EvaluateReply, std::string> parseEvaluateReply(std::string_view text) {
634 auto reply = detail::readMessage<EvaluateReply>(text, "the ui-evaluate reply", {}, {});
635 if (!reply) {
636 return std::unexpected(std::move(reply.error()));
637 }
638 if (!reply->body.is_object()) {
639 return std::unexpected("the ui-evaluate reply's body is not an object");
640 }
641 if (reply->errors.size() > kMaxCatalogFacts) {
642 return std::unexpected("the ui-evaluate reply carries more than " + std::to_string(kMaxCatalogFacts) +
643 " field errors");
644 }
645 return reply;
646}
647
648/// @brief Reads a `ui-file` request body.
649///
650/// Refused: a body longer than `kMaxFileRequestBytes`, text that is not JSON or nests too deep, a must-understand
651/// key, or an `op` that is not `begin`, `chunk`, `end` or `read`.
652/// @param text The request body.
653/// @return The request, or why it was refused.
654[[nodiscard]] inline std::expected<FileRequest, std::string> parseFileRequest(std::string_view text) {
655 if (text.size() > kMaxFileRequestBytes) {
656 return std::unexpected("the ui-file request is longer than " + std::to_string(kMaxFileRequestBytes) +
657 " bytes");
658 }
659 auto request = detail::readMessage<FileRequest>(text, "the ui-file request", {}, {});
660 if (!request) {
661 return std::unexpected(std::move(request.error()));
662 }
663 if (request->op != "begin" && request->op != "chunk" && request->op != "end" && request->op != "read") {
664 return std::unexpected("the ui-file request's op is not begin, chunk, end or read");
665 }
666 return request;
667}
668
669/// @brief Reads a `ui-file` reply body.
670/// @param text The reply body.
671/// @return The reply, or why it was refused.
672[[nodiscard]] inline std::expected<FileReply, std::string> parseFileReply(std::string_view text) {
673 return detail::readMessage<FileReply>(text, "the ui-file reply", {}, {});
674}
675
676/// @brief Reads a `ui-catalog` reply body.
677///
678/// Refused: text that is not JSON or nests too deep, a must-understand key at the top, in an entry or in a model's
679/// or action's facts, more than `kMaxBundleFiles` entries, more than `kMaxCatalogFacts` permitted actions, model
680/// facts or action facts, or an entry id longer than `kMaxNameBytes`.
681/// @param text The reply body.
682/// @return The reply, or why it was refused.
683[[nodiscard]] inline std::expected<CatalogReply, std::string> parseCatalogReply(std::string_view text) {
684 auto reply =
685 detail::readMessage<CatalogReply>(text, "the catalog", {"entries"}, {"models", "actions", "views", "wizards"});
686 if (!reply) {
687 return std::unexpected(std::move(reply.error()));
688 }
689 if (reply->entries.size() > kMaxBundleFiles) {
690 return std::unexpected("the catalog lists more than " + std::to_string(kMaxBundleFiles) + " entries");
691 }
692 std::size_t const models = reply->models ? reply->models->size() : 0;
693 std::size_t const actions = reply->actions ? reply->actions->size() : 0;
694 std::size_t const views = reply->views ? reply->views->size() : 0;
695 std::size_t const wizards = reply->wizards ? reply->wizards->size() : 0;
696 if (std::ranges::max({reply->permitted.size(), models, actions, views, wizards}) > kMaxCatalogFacts) {
697 return std::unexpected("the catalog carries more than " + std::to_string(kMaxCatalogFacts) +
698 " facts of a kind");
699 }
700 for (CatalogEntry const& entry : reply->entries) {
701 if (auto reason = detail::checkName(entry.id, "a catalog entry id")) {
702 return std::unexpected(std::move(*reason));
703 }
704 }
705 return reply;
706}
707
708/// @brief Reads a `ui-fetch` request body.
709///
710/// Refused: a body longer than `kMaxRequestBytes`, text that is not JSON or nests too deep, a must-understand key,
711/// or a hash that is not a content hash.
712/// @param text The request body.
713/// @return The request, or why it was refused.
714[[nodiscard]] inline std::expected<FetchRequest, std::string> parseFetchRequest(std::string_view text) {
715 if (text.size() > kMaxRequestBytes) {
716 return std::unexpected("the ui-fetch request is larger than " + std::to_string(kMaxRequestBytes) + " bytes");
717 }
718 auto request = detail::readMessage<FetchRequest>(text, "the ui-fetch request", {}, {});
719 if (!request) {
720 return std::unexpected(std::move(request.error()));
721 }
722 if (!isContentHash(request->hash)) {
723 return std::unexpected(std::string{"the ui-fetch request's hash is not a content hash"});
724 }
725 return request;
726}
727
728} // namespace morph::ui
constexpr std::string_view kHelloKind
The envelope kind of the handshake.
Definition bundle.hpp:39
constexpr std::size_t kMaxFileChunkBytes
The most file bytes one ui-file chunk carries, before base64url.
Definition bundle.hpp:64
constexpr std::string_view kFetchKind
The envelope kind that fetches a file by its hash.
Definition bundle.hpp:43
constexpr std::string_view kCatalogKind
The envelope kind of the catalog.
Definition bundle.hpp:41
bool isContentHash(std::string_view text)
Whether text is a well-formed content hash.
Definition bundle.hpp:115
constexpr std::string_view kScreenFile
The file kinds of a manifest.
Definition bundle.hpp:82
constexpr std::string_view kFetchAction
The pseudo-action that gates the fetch of a file that is not public.
Definition bundle.hpp:56
constexpr std::size_t kMaxBundleFiles
The most files a manifest, and entries a catalog, may list.
Definition bundle.hpp:73
std::expected< FileReply, std::string > parseFileReply(std::string_view text)
Reads a ui-file reply body.
Definition bundle.hpp:672
constexpr std::string_view kFileKind
The envelope kind of the file side channel (files/1, spec 6 §18): uploads and downloads in chunks.
Definition bundle.hpp:47
constexpr std::size_t kMaxRequestBytes
The largest request body (ui-hello, ui-fetch) a server reads.
Definition bundle.hpp:69
constexpr std::size_t kMaxListItems
The most items of a list or map in a ui-hello (vocabularies, host components, commands,...
Definition bundle.hpp:77
constexpr std::string_view kIconFile
An icon of a theme's icon set: SVG text.
Definition bundle.hpp:92
constexpr std::string_view kHelloAction
The pseudo-action that gates ui-hello, and the fetch of a public file or the manifest.
Definition bundle.hpp:52
constexpr std::size_t kMaxFileRequestBytes
The largest ui-file request body a server reads: a chunk in base64url, and the rest of the request.
Definition bundle.hpp:66
std::expected< EvaluateReply, std::string > parseEvaluateReply(std::string_view text)
Reads a ui-evaluate reply body.
Definition bundle.hpp:633
constexpr std::size_t kHashDigits
The hexadecimal digits of a SHA-256 digest.
Definition bundle.hpp:61
std::optional< std::string > checkName(std::string_view name, std::string_view what)
Checks a name's length.
Definition bundle.hpp:471
std::expected< HelloRequest, std::string > parseHelloRequest(std::string_view text)
Reads a ui-hello request body.
Definition bundle.hpp:574
std::optional< std::string > checkHostNames(std::map< std::string, std::int64_t, std::less<> > const &names, std::string_view list, std::string_view item)
Checks a map of host names to versions: its size and each name's length.
Definition bundle.hpp:503
std::expected< CatalogReply, std::string > parseCatalogReply(std::string_view text)
Reads a ui-catalog reply body.
Definition bundle.hpp:683
constexpr std::string_view kHashPrefix
The prefix of a content hash.
Definition bundle.hpp:59
std::optional< std::string > mustUnderstand(Json const &json, std::string_view where)
The must-understand key (!name) of one object, if it has one.
Definition bundle.hpp:369
std::optional< std::string > checkNames(std::vector< std::string > const &names, std::size_t limit, std::string_view list, std::string_view item)
Checks a list of names: its length and each name's.
Definition bundle.hpp:485
constexpr std::string_view kExprVocabulary
The expression vocabulary every bundle needs alongside ui/1.
Definition bundle.hpp:36
std::expected< HelloReply, std::string > parseHelloReply(std::string_view text)
Reads a ui-hello reply body.
Definition bundle.hpp:607
std::string writeMessage(T const &value)
Writes a message as JSON, escaping control bytes as wire::encode does.
Definition bundle.hpp:459
constexpr std::string_view kComponentFile
A delivered custom component (spec 5 §10).
Definition bundle.hpp:90
constexpr std::size_t kMaxIconBytes
The largest icon file a bundle may hold.
Definition bundle.hpp:94
std::expected< Manifest, std::string > parseManifest(std::string_view text)
Reads a manifest a server sent.
Definition bundle.hpp:532
std::expected< FileRequest, std::string > parseFileRequest(std::string_view text)
Reads a ui-file request body.
Definition bundle.hpp:654
std::optional< std::string > mustUnderstandIn(Json const &root, std::string_view what, std::initializer_list< std::string_view > arrays, std::initializer_list< std::string_view > maps)
The must-understand keys of a message: its top level, every element of its listed arrays and every va...
Definition bundle.hpp:391
std::string manifestText(Manifest const &manifest)
The manifest's text: the JSON its digest is the hash of. Equal manifests write equal text.
Definition bundle.hpp:523
constexpr std::string_view kServiceModel
The pseudo-model the service's envelopes are authorised under: authorize(session, "ui",...
Definition bundle.hpp:50
constexpr std::size_t kMaxFileBytes
The largest file a bundle may hold, and a client accepts.
Definition bundle.hpp:71
std::expected< FetchRequest, std::string > parseFetchRequest(std::string_view text)
Reads a ui-fetch request body.
Definition bundle.hpp:714
constexpr std::string_view kEvaluateKind
The envelope kind that evaluates an action body without running it (spec 5 §7).
Definition bundle.hpp:45
constexpr std::string_view kAppFile
The app document (spec 6 §5).
Definition bundle.hpp:84
constexpr std::size_t kMaxCatalogFacts
The most action ids, model facts or action facts a catalog may carry.
Definition bundle.hpp:75
constexpr std::string_view kCatalogAction
The pseudo-action that gates ui-catalog.
Definition bundle.hpp:54
constexpr std::string_view kMessagesFile
A message catalogue.
Definition bundle.hpp:88
std::string contentHash(std::string_view bytes)
The content hash of bytes: sha256: and the SHA-256 digest in 64 lower-case hexadecimal digits.
Definition bundle.hpp:99
std::expected< T, std::string > readMessage(std::string_view text, std::string_view what, std::initializer_list< std::string_view > arrays, std::initializer_list< std::string_view > maps)
Reads a message: bounded depth, must-understand keys refused, unknown keys ignored.
Definition bundle.hpp:437
constexpr std::size_t kMaxNameBytes
The longest name: an id, a kind, a vocabulary, a host name, an application id or version.
Definition bundle.hpp:79
constexpr std::string_view kThemeFile
A theme (ui-theme/1, spec 6 §6).
Definition bundle.hpp:86
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
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
Opt-in authenticated sessions: signed bearer tokens and a verifying IAuthorizer so a RemoteServer can...
std::string sha256(std::string_view data)
SHA-256 (FIPS 180-4). Returns the raw 32-byte digest.
One catalog entry (spec 6 §8).
Definition bundle.hpp:251
std::optional< std::string > host
The host screen the application registered with the client instead of a document.
Definition bundle.hpp:273
bool isPublic
Whether the client may fetch the screen before sign-in.
Definition bundle.hpp:271
bool singleton
Whether the screen opens at most once.
Definition bundle.hpp:263
std::string module
The navigator's module.
Definition bundle.hpp:257
Json title
The title: a string, or a message reference such as {"t": "lims.sample.title", "fallback": "…"}...
Definition bundle.hpp:255
std::optional< std::string > reason
Why the entry is not available: the application's reason, or denied.
Definition bundle.hpp:267
std::string icon
The icon name.
Definition bundle.hpp:261
std::string group
The navigator's group.
Definition bundle.hpp:259
bool reuseEmpty
Whether opening the entry replaces an empty, unmodified tab of it.
Definition bundle.hpp:275
bool available
Whether the entry can be opened; false lists it disabled with reason.
Definition bundle.hpp:265
std::string whenDenied
What a permission refusal does to the entry: hide or disable.
Definition bundle.hpp:269
std::string id
The screen's id.
Definition bundle.hpp:253
The body of a ui-catalog reply: the catalog filtered for the principal, and the facts a client's load...
Definition bundle.hpp:280
std::vector< CatalogEntry > entries
The entries the principal sees, in registration order.
Definition bundle.hpp:282
std::optional< std::map< std::string, ActionInfo, std::less<> > > actions
The action facts by action id; absent when the server has none to give.
Definition bundle.hpp:288
std::optional< std::map< std::string, std::string, std::less<> > > wizards
The w-* wizard documents a wizard node builds from, by the action of their first step; absent when th...
Definition bundle.hpp:294
Catalog facts() const
The facts as the lookups load-time validation takes. A lookup the reply carried no facts for is left ...
Definition bundle.hpp:299
std::optional< std::map< std::string, std::string, std::less<> > > views
The v-* view documents a collection node builds from, by the action of their v-query; absent when the...
Definition bundle.hpp:291
std::vector< std::string > permitted
The action ids the principal may run, which {"can": …} reads.
Definition bundle.hpp:284
std::optional< std::map< std::string, ModelInfo, std::less<> > > models
The model facts by type id; absent when the server has none to give.
Definition bundle.hpp:286
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)> 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
The body of a ui-evaluate reply: what ActionDispatcher::evaluate answered.
Definition bundle.hpp:197
bool ready
Whether an execute of this body would reach the model.
Definition bundle.hpp:203
std::map< std::string, std::string, std::less<> > errors
The field errors, by field name.
Definition bundle.hpp:201
Json body
The action body, reconciled, with its computed fields filled.
Definition bundle.hpp:199
The body of a ui-fetch request.
Definition bundle.hpp:332
std::string hash
The content hash of the file wanted: the manifest's digest, or a file the manifest lists.
Definition bundle.hpp:334
The body of a ui-file reply: the members its op answers, the others absent.
Definition bundle.hpp:233
std::optional< std::size_t > received
chunk: the bytes received so far.
Definition bundle.hpp:237
std::optional< std::string > ref
end: the file's reference.
Definition bundle.hpp:239
std::optional< std::string > upload
begin: the upload id.
Definition bundle.hpp:235
std::optional< std::size_t > total
read: the whole file's size.
Definition bundle.hpp:243
std::optional< std::string > name
read: the file's name.
Definition bundle.hpp:245
std::optional< std::string > type
read: its media type.
Definition bundle.hpp:247
std::optional< std::string > data
read: the bytes, base64url without padding.
Definition bundle.hpp:241
The body of a ui-file request: one step of an upload or a download, named by op.
Definition bundle.hpp:207
std::size_t size
begin: its size in bytes.
Definition bundle.hpp:215
std::size_t length
read: at most this many bytes; capped at kMaxFileChunkBytes.
Definition bundle.hpp:229
std::string data
chunk: the bytes, base64url without padding.
Definition bundle.hpp:225
std::string op
begin, chunk, end or read.
Definition bundle.hpp:209
std::string upload
chunk, end: the upload id begin answered.
Definition bundle.hpp:221
std::string action
begin: that mutation's action.
Definition bundle.hpp:219
std::size_t offset
chunk, read: the first byte.
Definition bundle.hpp:223
std::string name
begin: the file's name.
Definition bundle.hpp:211
std::string type
begin: its media type.
Definition bundle.hpp:213
std::string ref
read: the file's reference.
Definition bundle.hpp:227
std::string model
begin: the model type of the mutation that will reference the file.
Definition bundle.hpp:217
The body of a ui-hello reply.
Definition bundle.hpp:182
std::string application
The application's id.
Definition bundle.hpp:184
std::vector< std::string > vocabularies
The vocabularies the bundle needs; a client that lacks one is refused.
Definition bundle.hpp:188
bool events
Whether the server accepted the client's offer of events: it may send event envelopes on this connect...
Definition bundle.hpp:193
std::string digest
The manifest's content hash.
Definition bundle.hpp:190
std::string version
The application's version.
Definition bundle.hpp:186
The body of a ui-hello request: what the client speaks and carries.
Definition bundle.hpp:164
bool components
Whether the client loads delivered custom components.
Definition bundle.hpp:170
std::string qt
The client's Qt version; empty for a client without Qt.
Definition bundle.hpp:168
std::map< std::string, std::int64_t, std::less<> > hostCommands
The host commands the client has, by name, with their versions.
Definition bundle.hpp:174
std::vector< std::string > vocabularies
The vocabularies the client speaks, such as ui/1, expr/1, forms/1, ui-theme/1, files/1.
Definition bundle.hpp:166
std::map< std::string, std::int64_t, std::less<> > hostValues
The host values the client has, by name, with their versions.
Definition bundle.hpp:176
bool events
Whether the client offers to take server-sent event envelopes on this connection.
Definition bundle.hpp:178
std::map< std::string, std::int64_t, std::less<> > hostComponents
The host components the client has, by name, with their versions.
Definition bundle.hpp:172
One file of a manifest.
Definition bundle.hpp:125
std::string hash
The file's content hash.
Definition bundle.hpp:131
std::uint64_t size
The file's size in bytes.
Definition bundle.hpp:133
std::string kind
screen, app, theme, messages, component or icon; a client fetches no kind it does not know.
Definition bundle.hpp:129
std::string id
The file's id: a screen's document id, app, a theme's, a catalogue's or a component's name.
Definition bundle.hpp:127
bool operator==(ManifestFile const &) const =default
Field-wise equality.
bool isPublic
Whether anyone the ui-hello gate admits may fetch it, before sign-in (spec 6 §8 public).
Definition bundle.hpp:135
A bundle's manifest: every file the application serves, by its hash (spec 5 §12, "Content addressing"...
Definition bundle.hpp:143
ManifestFile const * find(std::string_view kind, std::string_view fileId) const
Finds a file.
Definition bundle.hpp:156
std::string application
The application's id.
Definition bundle.hpp:145
std::vector< ManifestFile > files
The files, sorted by kind then id.
Definition bundle.hpp:149
std::string version
The application's version.
Definition bundle.hpp:147
Write options that escape ASCII control bytes as \\uXXXX sequences.
Definition wire.hpp:401
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::expected< Json, std::string > parseJson(std::string_view text)
Parses JSON text into the document layer's JSON tree.
Definition value.hpp:752
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
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194