15#include "../core/completion.hpp"
17#include "../core/executor.hpp"
18#include "../core/model.hpp"
20#include "../core/registry.hpp"
21#include "../session/session.hpp"
50class IPreferencesStore {
52 IPreferencesStore() =
default;
53 IPreferencesStore(IPreferencesStore
const&) =
delete;
54 IPreferencesStore& operator=(IPreferencesStore
const&) =
delete;
55 IPreferencesStore(IPreferencesStore&&) =
delete;
56 IPreferencesStore& operator=(IPreferencesStore&&) =
delete;
57 virtual ~IPreferencesStore() =
default;
90 _strand.postTask([
this, entry = tie(key), settle = std::move(promise)]
mutable {
91 auto const found = _entries.find(entry);
92 settle.resolve(found == _entries.end() ? std::nullopt : std::optional<std::string>{found->second});
94 return std::move(completion);
103 _strand.postTask([
this, entry = tie(key),
text = std::move(json), settle = std::move(promise)]
mutable {
104 _entries.insert_or_assign(std::move(entry), std::move(
text));
105 settle.resolve(
true);
107 return std::move(completion);
115 _strand.postTask([
this, entry = tie(key), settle = std::move(promise)]
mutable {
116 settle.resolve(_entries.erase(entry) != 0);
118 return std::move(completion);
122 using Key = std::tuple<std::string, std::string, std::string, std::string>;
126 std::map<Key, std::string> _entries;
128 exec::OwnerStrand _strand;
159 std::map<std::string, std::string>
values;
209 : _store{std::move(store)}, _options{options} {
210 if (_store ==
nullptr) {
211 throw std::invalid_argument{
"PreferencesModel: a null store"};
220 checkName(request.app,
"app");
221 checkName(request.scope,
"scope");
223 throw std::invalid_argument{
"a load names more than " + std::to_string(
kMaxPrefPoints) +
" points"};
225 for (std::string
const& point : request.points) {
226 checkName(point,
"point");
229 std::string
const person = verifiedPerson();
230 if (person.empty()) {
233 for (std::string& point : request.points) {
234 std::optional<std::string> stored =
co_await _store->get(
235 PrefKey{.person = person, .app = request.app, .scope = request.scope, .point = point});
237 reply.
values.insert_or_assign(std::move(point), std::move(*stored));
248 PrefKey const key = keyOf(request.app, request.scope, request.point);
249 if (request.value.size() > _options.maxValueBytes) {
250 throw std::invalid_argument{
"a preference is longer than " + std::to_string(_options.maxValueBytes) +
253 if (
auto const parsed =
parseJson(request.value); !parsed) {
254 throw std::invalid_argument{
"a preference is JSON text: " + parsed.error()};
256 bool const stored =
co_await _store->set(key, std::move(request.value));
265 PrefKey const key = keyOf(request.app, request.scope, request.point);
266 bool const removed =
co_await _store->remove(key);
273 [[nodiscard]]
static std::string verifiedPerson() {
275 return session ==
nullptr ? std::string{} : session->principal;
284 [[nodiscard]]
static PrefKey keyOf(std::string
const& app, std::string
const& scope, std::string
const& point) {
285 checkName(app,
"app");
286 checkName(scope,
"scope");
287 checkName(point,
"point");
288 std::string person = verifiedPerson();
289 if (person.empty()) {
290 throw std::runtime_error{
"preferences are stored for a signed-in person"};
292 return PrefKey{.person = std::move(person), .app = app, .scope = scope, .point = point};
298 static void checkName(std::string
const& name, std::string_view what) {
299 if (name.empty() || name.size() > kMaxPrefNameBytes) {
300 throw std::invalid_argument{
"a preference's `" + std::string{what} +
"` is 1 to " +
301 std::to_string(kMaxPrefNameBytes) +
" bytes"};
305 std::shared_ptr<IPreferencesStore> _store;
306 PreferencesOptions _options;
315BRIDGE_DETAIL_ACTION_TRAITS_ONLY_BODY(morph::ui::PreferencesModel, morph::ui::PrefLoad, morph::ui::PrefValues,
"load",
316 ::morph::model::Loggable::No)
317BRIDGE_DETAIL_ACTION_TRAITS_ONLY_BODY(morph::ui::PreferencesModel, morph::ui::PrefSet, morph::ui::PrefStored,
"set",
318 ::morph::model::Loggable::No)
319BRIDGE_DETAIL_ACTION_TRAITS_ONLY_BODY(morph::ui::PreferencesModel, morph::ui::PrefRemove, morph::ui::PrefRemoved,
320 "remove", ::morph::model::Loggable::No)
336inline void registerPreferences(std::shared_ptr<IPreferencesStore> store, model::detail::ActionDispatcher& dispatcher,
338 if (store ==
nullptr) {
339 throw std::invalid_argument{
"registerPreferences: a null store"};
341 registry.registerModel<PreferencesModel>(
kPreferencesModel, [store = std::move(store), options] {
342 return std::make_unique<model::detail::ModelHolder<PreferencesModel>>(store, options);
344 dispatcher.registerAction<PreferencesModel, PrefLoad>(
kPreferencesModel,
"load");
346 dispatcher.registerAction<PreferencesModel, PrefRemove>(
kPreferencesModel,
"remove");
Move-only handle representing the eventual result of an asynchronous operation.
static std::pair< Completion< T >, Promise > makeSettleable(::morph::exec::IExecutor *execPtr)
Constructs a Completion<T>/Promise<T> pair sharing one settleable state.
virtual async::Completion< bool > set(PrefKey const &key, std::string json)=0
Writes a choice, replacing what was stored.
virtual async::Completion< bool > remove(PrefKey const &key)=0
Forgets a choice, so the point is at the document's default again.
virtual async::Completion< std::optional< std::string > > get(PrefKey const &key)=0
Reads a choice.
async::Completion< bool > set(PrefKey const &key, std::string json) override
Writes a choice.
async::Completion< bool > remove(PrefKey const &key) override
Forgets a choice.
MemoryPreferencesStore(exec::IExecutor &base)
A store whose strand runs on base.
async::Completion< std::optional< std::string > > get(PrefKey const &key) override
Reads a choice.
The server's preferences model: reads and writes the calling person's choices through a store.
::core::async::Task< PrefRemoved > execute(PrefRemove request)
Forgets the caller's choice.
::core::async::Task< PrefValues > execute(PrefLoad request)
Reads the caller's stored choices of request's points.
PreferencesModel(std::shared_ptr< IPreferencesStore > store, PreferencesOptions options)
A model over store.
::core::async::Task< PrefStored > execute(PrefSet request)
Stores the caller's choice.
Coroutines on core::async::Task: awaiting a Completion, starting a detached flow on a morph executor,...
OwnerStrand: one core-cpp strand over a morph executor, which is itself a morph executor — the owner ...
constexpr std::size_t kMaxPrefPoints
The most points one load may ask for.
constexpr std::string_view kPreferencesModel
The type id the preferences model is registered under.
constexpr std::size_t kMaxPrefNameBytes
The longest application, scope or point name a preferences request may carry, in bytes.
void registerPreferences(std::shared_ptr< IPreferencesStore > store, model::detail::ActionDispatcher &dispatcher, model::detail::ModelRegistryFactory ®istry, PreferencesOptions options={})
Registers the preferences model and its three actions, with a factory that gives every instance store...
Abstract executor interface.
Per-call session context carried through the bridge to the model.
Where one choice is stored.
std::string app
The application.
std::string person
The person, derived on the server from the verified principal; never taken from a request.
std::string point
The customization point.
std::string scope
The screen's id, or app for the app document's points.
load: the stored choices of a screen's points. A request carries no person.
std::vector< std::string > points
The points to read.
std::string app
The application.
std::string scope
The screen's id, or app.
remove: forgets one choice. A request carries no person.
std::string scope
The screen's id, or app.
std::string point
The point.
std::string app
The application.
bool removed
Whether an entry was there.
set: stores one choice. A request carries no person.
std::string point
The point.
std::string app
The application.
std::string value
The choice's JSON text.
std::string scope
The screen's id, or app.
bool stored
Whether the store took it.
std::map< std::string, std::string > values
The stored JSON text of each point that has an entry; a point without one is absent.
The preferences model's bounds.
std::size_t maxValueBytes
The longest JSON text a choice may have, in bytes.
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.
Node text(Text spec)
Static or bound text.
std::variant< std::int64_t, std::string > Key
The identity of a Switch case, a Select option, a ForEach or Table row and a drag payload.