morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
preference_sync.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <cstddef>
5#include <exception>
6#include <expected>
7#include <functional>
8#include <map>
9#include <memory>
10#include <string>
11#include <utility>
12#include <vector>
13
14#include "../core/callback_scope.hpp"
15#include "../core/completion.hpp"
16#include "document.hpp"
17#include "interpreter.hpp"
18#include "preferences.hpp"
19#include "value.hpp"
20
21/// @file
22/// @brief `PreferenceSync`: the client side of the preferences model. It loads a mounted screen's stored choices
23/// into `Screen::setPreference`, and writes what `InterpreterHost::preferenceChanged` and `resetPrefs` report.
24///
25/// Specified in `docs/spec/ui/preferences.md`.
26
27namespace morph::ui {
28
29/// @brief What loading a screen's choices did.
31 /// @brief Stored choices the screen took.
32 std::size_t applied = 0;
33 /// @brief Stored choices the screen ignored: text that is not a value of the point's type, or a value the point
34 /// no longer admits. The point keeps the document's default.
35 std::size_t ignored = 0;
36};
37
38/// @brief Talks to the server's preferences model (`morph.preferences`) for one application, through the
39/// interpreter's `Dispatcher`.
40///
41/// An application's `InterpreterHost` holds one, calls `load` after it mounts a screen, and forwards
42/// `preferenceChanged` to `changed` and `resetPrefs` to `reset`. No call names a person: the server keys every entry
43/// by the verified session's principal. All calls are made on the runtime's owner, where their replies arrive.
45public:
46 /// @brief A sync for @p application.
47 /// @param dispatcher Where model calls go. Borrowed: it must outlive the sync.
48 /// @param application The application's id, the `app` of every key.
49 /// @param onError Told the reason when a write fails; empty to ignore failures.
50 PreferenceSync(Dispatcher& dispatcher MORPH_LIFETIMEBOUND, std::string application,
51 std::function<void(std::string const&)> onError = {})
52 : _dispatcher{&dispatcher}, _application{std::move(application)}, _onError{std::move(onError)} {}
53
54 /// @brief Loads @p screen's stored choices, in one call for all its points, and sets each that fits.
55 ///
56 /// A point with no stored entry keeps the document's default. A stored entry that does not parse as the point's
57 /// type, or that the point refuses (`Screen::setPreference`), is ignored, so a new document version never breaks
58 /// on an old choice. A screen with no points makes no call and reports at once.
59 /// @param screen The mounted screen. Borrowed until the reply is delivered or @p scope stops.
60 /// @param scope Gates the delivery: stop or destroy it with the screen, and nothing touches the screen after.
61 /// @param done Called once with what was applied and ignored, or the reason the call failed; may be empty.
62 void load(Screen& screen, async::CallbackScope const& scope,
63 std::function<void(std::expected<PreferencesLoaded, std::string>)> done = {}) {
64 Document const& doc = screen.document();
65 PrefLoad request{.app = _application, .scope = doc.id, .points = {}};
66 for (auto const& [name, point] : doc.customize) {
67 static_cast<void>(point);
68 request.points.push_back(name);
69 }
70 _points.insert_or_assign(doc.id, request.points);
71 if (request.points.empty()) {
72 if (done) {
73 done(PreferencesLoaded{});
74 }
75 return;
76 }
77 auto const shared =
78 std::make_shared<std::function<void(std::expected<PreferencesLoaded, std::string>)>>(std::move(done));
79 channel()
80 ->execute("load", model::ActionTraits<PrefLoad>::toJson(request), CallOptions{})
81 .then(scope,
82 [&screen, shared](std::string const& reply) {
83 auto applied = apply(screen, reply);
84 if (*shared) {
85 (*shared)(std::move(applied));
86 }
87 })
88 .onError(scope, [shared](std::exception_ptr const& error) {
89 if (*shared) {
90 (*shared)(std::unexpected(detail::failureText(error)));
91 }
92 });
93 }
94
95 /// @brief Stores the user's choice at a point.
96 /// @param screen The screen's id.
97 /// @param point The point.
98 /// @param value The choice.
99 void changed(std::string const& screen, std::string const& point, Value const& value) {
100 PrefSet const request{.app = _application,
101 .scope = screen,
102 .point = point,
103 .value = glz::write_json(valueToJson(value)).value_or("null")};
104 report(channel()->execute("set", model::ActionTraits<PrefSet>::toJson(request), CallOptions{}));
105 }
106
107 /// @brief Forgets the stored choice at a point, or at every point of a screen this sync has loaded.
108 /// @param screen The screen's id.
109 /// @param point The point, or `all`.
110 // NOLINTNEXTLINE(bugprone-easily-swappable-parameters): InterpreterHost::resetPrefs's order.
111 void reset(std::string const& screen, std::string const& point) {
112 std::vector<std::string> points;
113 if (point == "all") {
114 if (auto const found = _points.find(screen); found != _points.end()) {
115 points = found->second;
116 }
117 } else {
118 points.push_back(point);
119 }
120 for (std::string& each : points) {
121 PrefRemove const request{.app = _application, .scope = screen, .point = std::move(each)};
122 report(channel()->execute("remove", model::ActionTraits<PrefRemove>::toJson(request), CallOptions{}));
123 }
124 }
125
126private:
127 /// @brief The binding, made at the first call.
128 /// @return It.
129 [[nodiscard]] std::shared_ptr<ModelChannel> const& channel() {
130 if (_channel == nullptr) {
131 _channel = _dispatcher->bind(std::string{kPreferencesModel}, bridge::BindSharing::Private, {});
132 }
133 return _channel;
134 }
135
136 /// @brief Tells `onError` when a write fails.
137 /// @param call The write.
138 void report(async::Completion<std::string> call) const {
139 call.thenDetached([](std::string const&) {})
140 .onErrorDetached([onError = _onError](std::exception_ptr const& error) {
141 if (onError) {
142 onError(detail::failureText(error));
143 }
144 });
145 }
146
147 /// @brief Sets each stored choice of a `load` reply that fits its point.
148 /// @param screen The screen.
149 /// @param reply The reply's JSON text.
150 /// @return What was applied and ignored, or the reason the reply is not one.
151 [[nodiscard]] static std::expected<PreferencesLoaded, std::string> apply(Screen& screen,
152 std::string const& reply) {
153 PrefValues values;
154 try {
155 values = model::ActionTraits<PrefLoad>::resultFromJson(reply);
156 } catch (std::exception const& error) {
157 return std::unexpected(std::string{"not a preferences reply: "} + error.what());
158 }
159 PreferencesLoaded loaded;
160 auto const& points = screen.document().customize;
161 for (auto& [name, text] : values.values) {
162 auto const point = points.find(name);
163 if (point == points.end() || !take(screen, point->second, text)) {
164 ++loaded.ignored;
165 continue;
166 }
167 ++loaded.applied;
168 }
169 return loaded;
170 }
171
172 /// @brief Parses a stored choice as its point's value and gives it to the screen.
173 /// @param screen The screen.
174 /// @param point The point.
175 /// @param text The stored JSON text.
176 /// @return Whether the screen took it.
177 [[nodiscard]] static bool take(Screen& screen, CustomizePoint const& point, std::string const& text) {
178 auto const json = parseJson(text);
179 if (!json) {
180 return false;
181 }
182 bool const typed = point.kind == "choice" || point.kind == "value";
183 auto value = typed ? valueFromJson(*json, point.type) : valueFromJson(*json);
184 return value && screen.setPreference(point.name, std::move(*value));
185 }
186
187 Dispatcher* _dispatcher;
188 std::string _application;
189 std::function<void(std::string const&)> _onError;
190 std::shared_ptr<ModelChannel> _channel;
191 /// @brief The points of each screen loaded so far, by screen id, so a reset of `all` knows what to remove.
192 std::map<std::string, std::vector<std::string>, std::less<>> _points;
193};
194
195} // namespace morph::ui
Screen screen(Screen::Builder builder)
A screen with its id as a template argument, as spec 5 §11 writes it.
Definition author.hpp:4556
Owned lifetime-and-stop gate for asynchronous callbacks — a data member, not a base class.
The interpreter's view of the bridge: binds a model by its type id, privately or to a shared instance...
virtual std::shared_ptr< ModelChannel > bind(std::string const &typeId, bridge::BindSharing sharing, std::string const &instanceKey)=0
Binds a model.
void load(Screen &screen, async::CallbackScope const &scope, std::function< void(std::expected< PreferencesLoaded, std::string >)> done={})
Loads screen's stored choices, in one call for all its points, and sets each that fits.
void reset(std::string const &screen, std::string const &point)
Forgets the stored choice at a point, or at every point of a screen this sync has loaded.
void changed(std::string const &screen, std::string const &point, Value const &value)
Stores the user's choice at a point.
PreferenceSync(Dispatcher &dispatcher, std::string application, std::function< void(std::string const &)> onError={})
A sync for application.
A mounted screen: its scopes, its view mounted through Mounted, and what its host asks of it.
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
The UI document interpreter: Interpreter::mount turns a loaded Document into reactive state and a vie...
Per-person storage of customization choices (spec 5 §12b, spec 6 §8): the IPreferencesStore seam,...
constexpr std::string_view kPreferencesModel
The type id the preferences model is registered under.
void request(Store< ViewState, Msg > &store, bridge::BridgeHandler< Model, Sharing > &handler, async::CallbackScope const &scope, Action action, ToMsg toMsg, ToFailMsg toFailMsg)
Executes action and sends its result, or its failure, to store as one message.
Definition store.hpp:202
Traits specialisation that maps an action type to its id, JSON codec, and result type.
Definition registry.hpp:48
How one model call is sent.
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, CustomizePoint, std::less<> > customize
Customization points, by name.
Definition document.hpp:568
load: the stored choices of a screen's points. A request carries no person.
remove: forgets one choice. A request carries no person.
set: stores one choice. A request carries no person.
What loading a screen's choices did.
std::size_t applied
Stored choices the screen took.
std::size_t ignored
Stored choices the screen ignored: text that is not a value of the point's type, or a value the point...
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
Json valueToJson(Value const &value)
Writes a value in its JSON form: the inverse of valueFromJson, with a timestamp as ISO-8601 text.
Definition value.hpp:668
std::expected< Value, std::string > valueFromJson(Json const &json)
Reads a value from its JSON form, structurally.
Definition value.hpp:570