morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
host_vocabulary.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <cstdint>
5#include <functional>
6#include <map>
7#include <set>
8#include <stdexcept>
9#include <string>
10#include <string_view>
11#include <utility>
12#include <vector>
13
14#include "bundle.hpp"
15#include "interpreter.hpp"
16#include "value.hpp"
17
18/// @file
19/// @brief `HostVocabulary`: the commands and values only the client application has (`host/1`, spec 6 §14),
20/// registered with their types and versions, run with checked arguments behind a trust gate, and declared to
21/// the server in `ui-hello`.
22///
23/// Specified in `docs/spec/ui/host_vocabulary.md`.
24
25namespace morph::ui {
26
27/// @brief One argument of a host command.
28struct HostArg {
29 /// @brief The argument's name, the key of the document's `args` record.
30 std::string name;
31 /// @brief Its document type, such as `string`, `int?` or a list of strings.
32 std::string type;
33};
34
35/// @brief A command the application registers, such as `restartApp` or `openPath`.
37 /// @brief The command's name, as `{"host": name}` gives it.
38 std::string name;
39 /// @brief Its version, declared in `ui-hello`.
40 std::int64_t version = 1;
41 /// @brief Its arguments. An optional type makes the argument optional.
42 std::vector<HostArg> args;
43 /// @brief Whether a document that came over a connection that is not verified may run it. A command that opens
44 /// files, reaches the network or touches the host is not.
45 bool safeForUntrusted = false;
46 /// @brief Runs the command with its checked arguments, by name; an absent optional argument is `null`.
47 std::function<void(ValueRecord const&)> run;
48};
49
50/// @brief A value the application registers, such as `sync.needsYouCount`.
51struct HostValue {
52 /// @brief The value's name, as `{"host": name}` reads it.
53 std::string name;
54 /// @brief Its version, declared in `ui-hello`.
55 std::int64_t version = 1;
56 /// @brief Its document type.
57 std::string type;
58 /// @brief Its value until the application sets another; it must have the type.
60};
61
62/// @brief The client's host vocabulary: what it registered, and whether its documents came over a verified
63/// connection.
64///
65/// An application builds one at start-up, registers its commands and values, connects it to the interpreter's
66/// `Session`, and tells it when the connection is verified (spec 5 §13: a verified TLS connection, or loopback with
67/// the development flag). Until then a command not marked `safeForUntrusted` is refused. It lives on the runtime's
68/// owner, like the session, and outlives the session it is connected to.
70public:
71 /// @brief Registers a command.
72 /// @param command The command. Refused (`std::invalid_argument`): an invalid name, a duplicate, an argument whose
73 /// name is empty or repeated or whose type does not parse, or no `run`.
74 void add(HostCommand command) {
75 checkName(command.name, "a host command");
76 if (_commands.contains(command.name)) {
77 throw std::invalid_argument{"host command `" + command.name + "` is registered twice"};
78 }
79 if (!command.run) {
80 throw std::invalid_argument{"host command `" + command.name + "` has nothing to run"};
81 }
82 Registered registered;
83 for (HostArg const& arg : command.args) {
84 if (arg.name.empty() || registered.args.contains(arg.name)) {
85 throw std::invalid_argument{"host command `" + command.name + "` has an empty or repeated argument"};
86 }
87 registered.args.emplace(arg.name, typeOf(arg.type, "argument `" + arg.name + "` of `" + command.name));
88 }
89 registered.command = std::move(command);
90 std::string name = registered.command.name;
91 _commands.emplace(std::move(name), std::move(registered));
92 publish();
93 }
94
95 /// @brief Registers a value.
96 /// @param value The value. Refused (`std::invalid_argument`): an invalid name, a duplicate, a type that does not
97 /// parse, or an initial value not of the type.
98 void add(HostValue value) {
99 checkName(value.name, "a host value");
100 if (_valueTypes.contains(value.name)) {
101 throw std::invalid_argument{"host value `" + value.name + "` is registered twice"};
102 }
103 Type type = typeOf(value.type, "host value `" + value.name);
104 if (!type.admits(value.initial)) {
105 throw std::invalid_argument{"the initial value of `" + value.name + "` is not a " + type.name()};
106 }
107 _valueTypes.emplace(value.name, std::move(type));
108 _valueVersions.emplace(value.name, value.version);
109 _values.insert_or_assign(std::move(value.name), std::move(value.initial));
110 publish();
111 }
112
113 /// @brief Changes a value; every binding on it updates.
114 /// @param name The value.
115 /// @param value Its new value.
116 /// @return Whether the value is registered and @p value has its type; otherwise nothing changes.
117 bool set(std::string const& name, Value value) {
118 auto const type = _valueTypes.find(name);
119 if (type == _valueTypes.end() || !type->second.admits(value)) {
120 return false;
121 }
122 _values.insert_or_assign(name, std::move(value));
123 if (_session != nullptr) {
124 _session->setHostValues(_values);
125 }
126 return true;
127 }
128
129 /// @brief Says whether the documents came over a verified connection. A command not marked `safeForUntrusted`
130 /// runs, and `can` is true for it, only while this is `true`. It starts `false`.
131 /// @param verified Whether the connection is verified.
132 void setConnectionVerified(bool verified) {
133 _verified = verified;
134 publish();
135 }
136
137 /// @brief Gives @p session the values, the names `{"can": "host:name"}` answers for, and the runner for
138 /// `{"host": name}` commands. Later registrations and changes reach it too.
139 /// @param session The session. Borrowed: this vocabulary outlives its use of it.
140 void connect(Session& session) {
141 _session = &session;
142 publish();
143 }
144
145 /// @brief Fills the host command and value names and versions of a `ui-hello` request, so the server knows what
146 /// this client can do before it serves a document that needs it.
147 /// @param hello The request.
148 void describe(HelloRequest& hello) const {
149 for (auto const& [name, registered] : _commands) {
150 hello.hostCommands.insert_or_assign(name, registered.command.version);
151 }
152 for (auto const& [name, version] : _valueVersions) {
153 hello.hostValues.insert_or_assign(name, version);
154 }
155 }
156
157 /// @brief Runs a command as a document's `{"host": name, "args": args}` does.
158 /// @param name The command.
159 /// @param args Its arguments: a record, or `null` for none.
160 /// @return `Ran`; `Missing` for a command nobody registered; `Refused`, with the reason, for an argument that is
161 /// missing, of the wrong type or not declared, or for an unsafe command on an unverified connection.
162 [[nodiscard]] HostCommandResult run(std::string const& name, Value const& args) const {
163 auto const found = _commands.find(name);
164 if (found == _commands.end()) {
165 return HostCommandResult{.kind = HostCommandResult::Kind::Missing, .reason = {}};
166 }
167 Registered const& registered = found->second;
168 if (!registered.command.safeForUntrusted && !_verified) {
169 return refused("host command `" + name + "` needs a verified connection");
170 }
171 ValueRecord checked;
172 ValueRecord const* given = args.asRecord();
173 if (given == nullptr && !args.isNull()) {
174 return refused("the arguments of host command `" + name + "` are a record");
175 }
176 if (given != nullptr) {
177 for (auto const& [argName, value] : *given) {
178 static_cast<void>(value);
179 if (!registered.args.contains(argName)) {
180 std::string reason = "host command `" + name;
181 reason += "` has no argument `";
182 reason += argName;
183 reason += '`';
184 return refused(std::move(reason));
185 }
186 }
187 }
188 for (auto const& [argName, type] : registered.args) {
189 Value value;
190 if (given != nullptr) {
191 if (auto const supplied = given->find(argName); supplied != given->end()) {
192 value = supplied->second;
193 }
194 }
195 if (!type.admits(value)) {
196 std::string reason = "argument `" + argName;
197 reason += "` of host command `";
198 reason += name;
199 reason += "` is not a ";
200 reason += type.name();
201 return refused(std::move(reason));
202 }
203 checked.insert_or_assign(argName, std::move(value));
204 }
205 registered.command.run(checked);
206 return HostCommandResult{.kind = HostCommandResult::Kind::Ran, .reason = {}};
207 }
208
209private:
210 struct Registered {
211 HostCommand command;
212 std::map<std::string, Type, std::less<>> args;
213 };
214
215 [[nodiscard]] static HostCommandResult refused(std::string reason) {
216 return HostCommandResult{.kind = HostCommandResult::Kind::Refused, .reason = std::move(reason)};
217 }
218
219 static void checkName(std::string const& name, std::string_view what) {
220 if (name.empty()) {
221 throw std::invalid_argument{std::string{what} + " has a name"};
222 }
223 if (auto reason = detail::checkName(name, what)) {
224 throw std::invalid_argument{*reason};
225 }
226 }
227
228 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): the type's text, then what it types, as the reason reads.
229 [[nodiscard]] static Type typeOf(std::string const& text, std::string const& what) {
230 auto type = parseType(text);
231 if (!type) {
232 throw std::invalid_argument{"the type of " + what + "` does not parse: " + type.error()};
233 }
234 return std::move(*type);
235 }
236
237 /// Hands the session the values and the commands runnable now.
238 void publish() {
239 if (_session == nullptr) {
240 return;
241 }
242 std::set<std::string, std::less<>> available;
243 for (auto const& [name, registered] : _commands) {
244 if (registered.command.safeForUntrusted || _verified) {
245 available.insert(name);
246 }
247 }
248 _session->runtime().batch([&] {
249 _session->setHostValues(_values);
250 _session->setHostCommands(
251 HostCommands{.available = std::move(available),
252 .run = [this](std::string const& name, Value const& args) { return run(name, args); }});
253 });
254 }
255
256 std::map<std::string, Registered, std::less<>> _commands;
257 std::map<std::string, Type, std::less<>> _valueTypes;
258 std::map<std::string, std::int64_t, std::less<>> _valueVersions;
259 ValueRecord _values;
260 Session* _session = nullptr;
261 bool _verified = false;
262};
263
264} // namespace morph::ui
What the UI service's client and server share (spec 5 §12): content hashes, the bundle's manifest,...
The client's host vocabulary: what it registered, and whether its documents came over a verified conn...
void connect(Session &session)
Gives session the values, the names {"can": "host:name"} answers for, and the runner for {"host": nam...
void add(HostCommand command)
Registers a command.
HostCommandResult run(std::string const &name, Value const &args) const
Runs a command as a document's {"host": name, "args": args} does.
void add(HostValue value)
Registers a value.
bool set(std::string const &name, Value value)
Changes a value; every binding on it updates.
void describe(HelloRequest &hello) const
Fills the host command and value names and versions of a ui-hello request, so the server knows what t...
void setConnectionVerified(bool verified)
Says whether the documents came over a verified connection. A command not marked safeForUntrusted run...
What screens of one client session share (spec 5 §5, §12b): the query cache, the mounted queries,...
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
bool isNull() const noexcept
Whether the value is null.
Definition value.hpp:212
ValueRecord const * asRecord() const noexcept
The record, if held.
Definition value.hpp:247
@ Session
{"session": "name"}: a session attribute.
The UI document interpreter: Interpreter::mount turns a loaded Document into reactive state and a vie...
The body of a ui-hello request: what the client speaks and carries.
Definition bundle.hpp:164
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::map< std::string, std::int64_t, std::less<> > hostValues
The host values the client has, by name, with their versions.
Definition bundle.hpp:176
One argument of a host command.
std::string name
The argument's name, the key of the document's args record.
std::string type
Its document type, such as string, int? or a list of strings.
What running a host command (host/1) did.
@ Missing
The client has no such command; nothing ran.
@ Refused
The command exists and did not run: its arguments, or a connection that is not verified.
A command the application registers, such as restartApp or openPath.
std::string name
The command's name, as {"host": name} gives it.
std::int64_t version
Its version, declared in ui-hello.
std::function< void(ValueRecord const &)> run
Runs the command with its checked arguments, by name; an absent optional argument is null.
bool safeForUntrusted
Whether a document that came over a connection that is not verified may run it. A command that opens ...
std::vector< HostArg > args
Its arguments. An optional type makes the argument optional.
A value the application registers, such as sync.needsYouCount.
Value initial
Its value until the application sets another; it must have the type.
std::int64_t version
Its version, declared in ui-hello.
std::string type
Its document type.
std::string name
The value's name, as {"host": name} reads it.
A document type (spec 5 §3): a kind, whether null is allowed, and a list's element type.
Definition value.hpp:311
std::string name() const
The type as a document spells it.
Definition value.hpp:347
bool admits(Value const &value) const
Whether a value conforms to the type.
Definition value.hpp:367
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
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::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Definition value.hpp:129