9#include <glaze/glaze.hpp>
10#include <initializer_list>
22#include "../core/wire.hpp"
39inline constexpr std::string_view
kHelloKind =
"ui-hello";
43inline constexpr std::string_view
kFetchKind =
"ui-fetch";
47inline constexpr std::string_view
kFileKind =
"ui-file";
71inline constexpr std::size_t
kMaxFileBytes = std::size_t{4} * 1024 * 1024;
84inline constexpr std::string_view
kAppFile =
"app";
92inline constexpr std::string_view
kIconFile =
"icon";
99[[nodiscard]]
inline std::string
contentHash(std::string_view bytes) {
100 static constexpr std::string_view kHex =
"0123456789abcdef";
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)));
119 return std::ranges::all_of(
text.substr(
kHashPrefix.size()), [](
char const digit) {
120 return (digit >=
'0' && digit <=
'9') || (digit >=
'a' && digit <=
'f');
156 [[nodiscard]]
ManifestFile const*
find(std::string_view kind, std::string_view fileId)
const {
157 auto const found = std::ranges::find_if(
159 return found ==
files.end() ? nullptr : &*found;
201 std::map<std::string, std::string, std::less<>>
errors;
239 std::optional<std::string>
ref;
241 std::optional<std::string>
data;
245 std::optional<std::string>
name;
247 std::optional<std::string>
type;
273 std::optional<std::string>
host;
286 std::optional<std::map<std::string, ModelInfo, std::less<>>>
models;
288 std::optional<std::map<std::string, ActionInfo, std::less<>>>
actions;
291 std::optional<std::map<std::string, std::string, std::less<>>>
views;
294 std::optional<std::map<std::string, std::string, std::less<>>>
wizards;
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()) {
308 return found->second;
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()) {
318 return found->second;
345 static constexpr auto value =
346 glz::object(
"id", &T::id,
"kind", &T::kind,
"hash", &T::hash,
"size", &T::size,
"public", &T::isPublic);
351struct glz::meta<morph::ui::CatalogEntry> {
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);
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) {
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";
392 std::initializer_list<std::string_view> arrays,
393 std::initializer_list<std::string_view> maps) {
397 auto const*
const object = std::get_if<Json::object_t>(&root.data);
398 if (
object ==
nullptr) {
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) {
407 for (
Json const& item : *items) {
408 if (
auto reason =
mustUnderstand(item, std::string{what} +
" " + std::string{key})) {
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) {
419 for (
auto const& [name, item] : *items) {
420 if (
auto reason =
mustUnderstand(item, std::string{what} +
" " + std::string{key} +
" " + name)) {
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) {
442 return std::unexpected(std::string{what} +
": " + tree.error());
445 return std::unexpected(std::move(*reason));
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));
471[[nodiscard]]
inline std::optional<std::string>
checkName(std::string_view name, std::string_view what) {
473 return std::string{what} +
" is longer than " + std::to_string(
kMaxNameBytes) +
" bytes";
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";
490 for (std::string
const& name : names) {
491 if (
auto reason =
checkName(name, item)) {
504 std::map<std::string, std::int64_t, std::less<>>
const& names, std::string_view list, std::string_view item) {
506 return std::string{list} +
" list more than " + std::to_string(
kMaxListItems) +
" items";
508 for (
auto const& [name, version] : names) {
509 static_cast<void>(version);
510 if (
auto reason =
checkName(name, item)) {
535 return std::unexpected(std::move(manifest.error()));
538 return std::unexpected(
"the manifest lists more than " + std::to_string(
kMaxBundleFiles) +
" files");
540 if (
auto reason =
detail::checkName(manifest->application,
"the application id")) {
541 return std::unexpected(std::move(*reason));
543 if (
auto reason =
detail::checkName(manifest->version,
"the application version")) {
544 return std::unexpected(std::move(*reason));
546 std::set<std::pair<std::string_view, std::string_view>> seen;
549 return std::unexpected(std::move(*reason));
552 return std::unexpected(std::move(*reason));
555 return std::unexpected(
"the manifest file `" + file.
id +
"` has a malformed hash");
558 return std::unexpected(
"the manifest file `" + file.
id +
"` is larger than " +
561 if (!seen.emplace(file.
kind, file.
id).second) {
562 return std::unexpected(
"the manifest lists the " + file.
kind +
" `" + file.
id +
"` twice");
576 return std::unexpected(
"the ui-hello request is larger than " + std::to_string(
kMaxRequestBytes) +
" bytes");
579 "the ui-hello request", {}, {});
581 return std::unexpected(std::move(request.error()));
584 return std::unexpected(std::move(*reason));
587 return std::unexpected(std::move(*reason));
589 if (
auto reason =
detail::checkHostNames(request->hostComponents,
"the host components",
"a host component")) {
590 return std::unexpected(std::move(*reason));
593 return std::unexpected(std::move(*reason));
596 return std::unexpected(std::move(*reason));
610 return std::unexpected(std::move(reply.error()));
613 return std::unexpected(std::move(*reason));
616 return std::unexpected(std::move(*reason));
618 if (
auto reason =
detail::checkName(reply->version,
"the application version")) {
619 return std::unexpected(std::move(*reason));
622 return std::unexpected(
"the ui-hello reply's digest is not a content hash");
636 return std::unexpected(std::move(reply.error()));
638 if (!reply->body.is_object()) {
639 return std::unexpected(
"the ui-evaluate reply's body is not an object");
642 return std::unexpected(
"the ui-evaluate reply carries more than " + std::to_string(
kMaxCatalogFacts) +
656 return std::unexpected(
"the ui-file request is longer than " + std::to_string(
kMaxFileRequestBytes) +
661 return std::unexpected(std::move(request.error()));
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");
687 return std::unexpected(std::move(reply.error()));
690 return std::unexpected(
"the catalog lists more than " + std::to_string(
kMaxBundleFiles) +
" entries");
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) +
702 return std::unexpected(std::move(*reason));
716 return std::unexpected(
"the ui-fetch request is larger than " + std::to_string(
kMaxRequestBytes) +
" bytes");
720 return std::unexpected(std::move(request.error()));
723 return std::unexpected(std::string{
"the ui-fetch request's hash is not a content hash"});
constexpr std::string_view kHelloKind
The envelope kind of the handshake.
constexpr std::size_t kMaxFileChunkBytes
The most file bytes one ui-file chunk carries, before base64url.
constexpr std::string_view kFetchKind
The envelope kind that fetches a file by its hash.
constexpr std::string_view kCatalogKind
The envelope kind of the catalog.
bool isContentHash(std::string_view text)
Whether text is a well-formed content hash.
constexpr std::string_view kScreenFile
The file kinds of a manifest.
constexpr std::string_view kFetchAction
The pseudo-action that gates the fetch of a file that is not public.
constexpr std::size_t kMaxBundleFiles
The most files a manifest, and entries a catalog, may list.
std::expected< FileReply, std::string > parseFileReply(std::string_view text)
Reads a ui-file reply body.
constexpr std::string_view kFileKind
The envelope kind of the file side channel (files/1, spec 6 §18): uploads and downloads in chunks.
constexpr std::size_t kMaxRequestBytes
The largest request body (ui-hello, ui-fetch) a server reads.
constexpr std::size_t kMaxListItems
The most items of a list or map in a ui-hello (vocabularies, host components, commands,...
constexpr std::string_view kIconFile
An icon of a theme's icon set: SVG text.
constexpr std::string_view kHelloAction
The pseudo-action that gates ui-hello, and the fetch of a public file or the manifest.
constexpr std::size_t kMaxFileRequestBytes
The largest ui-file request body a server reads: a chunk in base64url, and the rest of the request.
std::expected< EvaluateReply, std::string > parseEvaluateReply(std::string_view text)
Reads a ui-evaluate reply body.
constexpr std::size_t kHashDigits
The hexadecimal digits of a SHA-256 digest.
std::optional< std::string > checkName(std::string_view name, std::string_view what)
Checks a name's length.
std::expected< HelloRequest, std::string > parseHelloRequest(std::string_view text)
Reads a ui-hello request body.
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.
std::expected< CatalogReply, std::string > parseCatalogReply(std::string_view text)
Reads a ui-catalog reply body.
constexpr std::string_view kHashPrefix
The prefix of a content hash.
std::optional< std::string > mustUnderstand(Json const &json, std::string_view where)
The must-understand key (!name) of one object, if it has one.
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.
constexpr std::string_view kExprVocabulary
The expression vocabulary every bundle needs alongside ui/1.
std::expected< HelloReply, std::string > parseHelloReply(std::string_view text)
Reads a ui-hello reply body.
std::string writeMessage(T const &value)
Writes a message as JSON, escaping control bytes as wire::encode does.
constexpr std::string_view kComponentFile
A delivered custom component (spec 5 §10).
constexpr std::size_t kMaxIconBytes
The largest icon file a bundle may hold.
std::expected< Manifest, std::string > parseManifest(std::string_view text)
Reads a manifest a server sent.
std::expected< FileRequest, std::string > parseFileRequest(std::string_view text)
Reads a ui-file request body.
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...
std::string manifestText(Manifest const &manifest)
The manifest's text: the JSON its digest is the hash of. Equal manifests write equal text.
constexpr std::string_view kServiceModel
The pseudo-model the service's envelopes are authorised under: authorize(session, "ui",...
constexpr std::size_t kMaxFileBytes
The largest file a bundle may hold, and a client accepts.
std::expected< FetchRequest, std::string > parseFetchRequest(std::string_view text)
Reads a ui-fetch request body.
constexpr std::string_view kEvaluateKind
The envelope kind that evaluates an action body without running it (spec 5 §7).
constexpr std::string_view kAppFile
The app document (spec 6 §5).
constexpr std::size_t kMaxCatalogFacts
The most action ids, model facts or action facts a catalog may carry.
constexpr std::string_view kCatalogAction
The pseudo-action that gates ui-catalog.
constexpr std::string_view kMessagesFile
A message catalogue.
std::string contentHash(std::string_view bytes)
The content hash of bytes: sha256: and the SHA-256 digest in 64 lower-case hexadecimal digits.
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.
constexpr std::size_t kMaxNameBytes
The longest name: an id, a kind, a vocabulary, a host name, an application id or version.
constexpr std::string_view kThemeFile
A theme (ui-theme/1, spec 6 §6).
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.
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).
std::optional< std::string > host
The host screen the application registered with the client instead of a document.
bool isPublic
Whether the client may fetch the screen before sign-in.
bool singleton
Whether the screen opens at most once.
std::string module
The navigator's module.
Json title
The title: a string, or a message reference such as {"t": "lims.sample.title", "fallback": "…"}...
std::optional< std::string > reason
Why the entry is not available: the application's reason, or denied.
std::string icon
The icon name.
std::string group
The navigator's group.
bool reuseEmpty
Whether opening the entry replaces an empty, unmodified tab of it.
bool available
Whether the entry can be opened; false lists it disabled with reason.
std::string whenDenied
What a permission refusal does to the entry: hide or disable.
std::string id
The screen's id.
The body of a ui-catalog reply: the catalog filtered for the principal, and the facts a client's load...
std::vector< CatalogEntry > entries
The entries the principal sees, in registration order.
std::optional< std::map< std::string, ActionInfo, std::less<> > > actions
The action facts by action id; absent when the server has none to give.
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...
Catalog facts() const
The facts as the lookups load-time validation takes. A lookup the reply carried no facts for is left ...
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...
std::vector< std::string > permitted
The action ids the principal may run, which {"can": …} reads.
std::optional< std::map< std::string, ModelInfo, std::less<> > > models
The model facts by type id; absent when the server has none to give.
The facts load-time validation needs from the application's catalog. Local mode answers them from the...
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...
std::function< std::optional< ModelInfo >(std::string_view typeId)> model
Looks up a model type by its registered type id.
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...
std::function< std::optional< ActionInfo >(std::string_view actionId)> action
Looks up an action by its id.
The body of a ui-evaluate reply: what ActionDispatcher::evaluate answered.
bool ready
Whether an execute of this body would reach the model.
std::map< std::string, std::string, std::less<> > errors
The field errors, by field name.
Json body
The action body, reconciled, with its computed fields filled.
The body of a ui-fetch request.
std::string hash
The content hash of the file wanted: the manifest's digest, or a file the manifest lists.
The body of a ui-file reply: the members its op answers, the others absent.
std::optional< std::size_t > received
chunk: the bytes received so far.
std::optional< std::string > ref
end: the file's reference.
std::optional< std::string > upload
begin: the upload id.
std::optional< std::size_t > total
read: the whole file's size.
std::optional< std::string > name
read: the file's name.
std::optional< std::string > type
read: its media type.
std::optional< std::string > data
read: the bytes, base64url without padding.
The body of a ui-file request: one step of an upload or a download, named by op.
std::size_t size
begin: its size in bytes.
std::size_t length
read: at most this many bytes; capped at kMaxFileChunkBytes.
std::string data
chunk: the bytes, base64url without padding.
std::string op
begin, chunk, end or read.
std::string upload
chunk, end: the upload id begin answered.
std::string action
begin: that mutation's action.
std::size_t offset
chunk, read: the first byte.
std::string name
begin: the file's name.
std::string type
begin: its media type.
std::string ref
read: the file's reference.
std::string model
begin: the model type of the mutation that will reference the file.
The body of a ui-hello reply.
std::string application
The application's id.
std::vector< std::string > vocabularies
The vocabularies the bundle needs; a client that lacks one is refused.
bool events
Whether the server accepted the client's offer of events: it may send event envelopes on this connect...
std::string digest
The manifest's content hash.
std::string version
The application's version.
The body of a ui-hello request: what the client speaks and carries.
bool components
Whether the client loads delivered custom components.
std::string qt
The client's Qt version; empty for a client without Qt.
std::map< std::string, std::int64_t, std::less<> > hostCommands
The host commands the client has, by name, with their versions.
std::vector< std::string > vocabularies
The vocabularies the client speaks, such as ui/1, expr/1, forms/1, ui-theme/1, files/1.
std::map< std::string, std::int64_t, std::less<> > hostValues
The host values the client has, by name, with their versions.
bool events
Whether the client offers to take server-sent event envelopes on this connection.
std::map< std::string, std::int64_t, std::less<> > hostComponents
The host components the client has, by name, with their versions.
std::string hash
The file's content hash.
std::uint64_t size
The file's size in bytes.
std::string kind
screen, app, theme, messages, component or icon; a client fetches no kind it does not know.
std::string id
The file's id: a screen's document id, app, a theme's, a catalogue's or a component's name.
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).
A bundle's manifest: every file the application serves, by its hash (spec 5 §12, "Content addressing"...
ManifestFile const * find(std::string_view kind, std::string_view fileId) const
Finds a file.
std::string application
The application's id.
std::vector< ManifestFile > files
The files, sorted by kind then id.
std::string version
The application's version.
Write options that escape ASCII control bytes as \\uXXXX sequences.
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.
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
Node text(Text spec)
Static or bound text.