morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
effects.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <cassert>
6#include <cstddef>
7#include <cstdint>
8#include <functional>
9#include <iterator>
10#include <map>
11#include <mutex>
12#include <optional>
13#include <stdexcept>
14#include <string>
15#include <string_view>
16#include <type_traits>
17#include <typeindex>
18#include <typeinfo>
19#include <utility>
20#include <vector>
21
23#include "records.hpp"
24#include "registry.hpp"
25
26/// @file
27/// What an action changes, and which records its result holds.
28///
29/// `BRIDGE_REGISTER_EFFECTS` records an action's effects as data under its id.
30/// Nothing is matched or checked at registration, so the order of record,
31/// action and effects registrations across translation units does not matter.
32/// The first read through `effectsOf`, `recordsOf`, `recordNamed` or
33/// `actionsHolding` matches every action's result slots against the record
34/// registry and validates the whole configuration, once. A configuration with
35/// problems throws `morph::model::EffectsRegistrationError` naming all of them,
36/// on that read and on every later one.
37
38namespace morph::model {
39
40/// @brief Thrown on a read of the effects or record registry when the registered configuration is invalid.
41///
42/// Lists every problem found, one per line, not only the first. Matching runs on
43/// the first read, not when the registration phase closes, because
44/// `closeRegistrationPhase()` is `noexcept` and a release build closes it only
45/// when the application asks.
46struct EffectsRegistrationError : std::logic_error {
47 /// @brief Constructs the error from the problems found.
48 /// @param problems One sentence per problem.
49 explicit EffectsRegistrationError(std::vector<std::string> problems)
50 : std::logic_error(message(problems)), _problems(std::move(problems)) {}
51
52 /// @brief The problems found, in the order they were found.
53 /// @return One sentence per problem.
54 [[nodiscard]] std::vector<std::string> const& problems() const noexcept { return _problems; }
55
56private:
57 static std::string message(std::vector<std::string> const& problems) {
58 std::string text = "morph: the registered records and effects are invalid:";
59 for (auto const& problem : problems) {
60 text += "\n - ";
61 text += problem;
62 }
63 return text;
64 }
65
66 std::vector<std::string> _problems;
67};
68
69} // namespace morph::model
70
71namespace morph::effects {
72
73/// @brief Effect: the action edits existing rows of record @p R, and its result carries them.
74/// @tparam R A keyed record type that the action's result holds.
75template <typename R>
76struct updates {};
77
78/// @brief Effect: the action may create, remove or change rows of record @p R, which its result need not carry.
79/// @tparam R A record type.
80template <typename R>
81struct changes {};
82
83/// @brief Effect: the state of the instance the action ran on changed.
85
86/// @brief Effect: the action changes nothing, declared explicitly.
87struct none {};
88
89/// @brief The kind of one declared effect.
90enum class Kind : std::uint8_t {
91 Updates, ///< `updates<R>`
92 Changes, ///< `changes<R>`
93 ChangesInstance, ///< `changesInstance`
94 None, ///< `none`
95};
96
97/// @brief One effect of an action, with its record resolved to a wire name.
98struct Effect {
99 /// What the effect is.
101 /// The record's wire name; empty for `ChangesInstance` and `None`.
102 std::string record;
103
104 /// @brief Memberwise equality.
105 /// @param lhs One effect.
106 /// @param rhs The other.
107 /// @return True when kind and record match.
108 friend bool operator==(Effect const& lhs, Effect const& rhs) = default;
109};
110
111/// @brief A record an action's result holds, and where.
113 /// The record's wire name.
114 std::string record;
115 /// The record's key field, empty for a keyless record.
116 std::optional<std::string> keyField;
117 /// The JSON paths at which the result holds the record; `""` is the result itself.
118 std::vector<std::string> paths;
119
120 /// @brief Memberwise equality.
121 /// @param lhs One entry.
122 /// @param rhs The other.
123 /// @return True when record, key field and paths match.
124 friend bool operator==(ActionRecord const& lhs, ActionRecord const& rhs) = default;
125};
126
127namespace detail {
128
129/// @brief One effect as a registrar records it: the record still a type, not yet a name.
131 /// What the effect is.
133 /// The record type, for `Updates` and `Changes`.
134 std::optional<std::type_index> record;
135
136 /// @brief Memberwise equality.
137 /// @param lhs One effect.
138 /// @param rhs The other.
139 /// @return True when kind and record match.
140 friend bool operator==(DeclaredEffect const& lhs, DeclaredEffect const& rhs) = default;
141};
142
143/// @brief Every distinct effects declaration, by action id. More than one for an id is a conflict.
144using EffectsMap = std::map<std::string, std::vector<std::vector<DeclaredEffect>>, std::less<>>;
145
146/// @brief Maps an effect tag type to its `DeclaredEffect`.
147/// @tparam E An effect tag.
148template <typename E>
149struct EffectOf {
150 static_assert(sizeof(E) == 0,
151 "BRIDGE_REGISTER_EFFECTS: an effect is updates<R>, changes<R>, changesInstance or none");
152};
153template <typename R>
154struct EffectOf<updates<R>> {
155 /// @brief The declared effect.
156 /// @return `Updates` on @p R.
157 static DeclaredEffect value() { return {.kind = Kind::Updates, .record = std::type_index{typeid(R)}}; }
158};
159template <typename R>
160struct EffectOf<changes<R>> {
161 /// @brief The declared effect.
162 /// @return `Changes` on @p R.
163 static DeclaredEffect value() { return {.kind = Kind::Changes, .record = std::type_index{typeid(R)}}; }
164};
165template <>
166struct EffectOf<changesInstance> {
167 /// @brief The declared effect.
168 /// @return `ChangesInstance`.
169 static DeclaredEffect value() { return {.kind = Kind::ChangesInstance, .record = std::nullopt}; }
170};
171template <>
172struct EffectOf<none> {
173 /// @brief The declared effect.
174 /// @return `None`.
175 static DeclaredEffect value() { return {.kind = Kind::None, .record = std::nullopt}; }
176};
177
178/// @brief Adds one declaration for an action, unless an equal one is already there.
179///
180/// A declaration is a set: a repeated effect is stored once, and two
181/// declarations holding the same effects in any order are equal, so one
182/// repeated in several translation units is stored once.
183/// @param map The registry to add to.
184/// @param actionId The action's id.
185/// @param declared The effects.
186inline void addEffects(EffectsMap& map, std::string_view actionId, std::vector<DeclaredEffect> declared) {
187 auto entry = map.find(actionId);
188 if (entry == map.end()) {
189 entry = map.emplace(std::string{actionId}, std::vector<std::vector<DeclaredEffect>>{}).first;
190 }
191 std::vector<DeclaredEffect> distinct;
192 for (auto& effect : declared) {
193 if (std::ranges::find(distinct, effect) == distinct.end()) {
194 distinct.push_back(std::move(effect));
195 }
196 }
197 auto& declarations = entry->second;
198 bool const known = std::ranges::any_of(declarations, [&](std::vector<DeclaredEffect> const& existing) {
199 return std::ranges::is_permutation(existing, distinct);
200 });
201 if (!known) {
202 declarations.push_back(std::move(distinct));
203 }
204}
205
206/// @brief The process-level effects registry, appended to by static registrars.
207/// @return The mutable map.
209 static EffectsMap entries;
210 return entries;
211}
212
213/// @brief Static-init helper for `BRIDGE_REGISTER_EFFECTS`.
214/// @tparam Action The action type; `ActionTraits<Action>` must be visible.
215/// @tparam E The effect tags.
216/// @return Always true, so the result can initialise a namespace-scope variable.
217template <typename Action, typename... E>
218inline bool registerEffectsOnce() noexcept {
219 static_assert(sizeof...(E) > 0, "BRIDGE_REGISTER_EFFECTS: declare at least one effect");
220 // NOLINTNEXTLINE(misc-static-assert,cert-dcl03-c)
221 assert(!::morph::model::registrationPhaseClosed() &&
222 "registerEffectsOnce: registration after the registration phase closed. The process-level "
223 "registries are unsynchronised and are read-only once dispatch begins -- registering now "
224 "races their internals against concurrent lookups. Load and register plugin modules before "
225 "the first dispatch.");
227 std::vector<DeclaredEffect>{EffectOf<E>::value()...});
228 return true;
229}
230
231/// @brief The matched and validated registries: what every read is answered from.
232struct Resolved {
233 /// @brief The records, one entry per registered type, in registration order.
234 std::vector<record::RecordInfo> records;
235 /// @brief Each action's effects, by action id; actions that declared none are absent.
236 std::map<std::string, std::vector<Effect>, std::less<>> effects;
237 /// @brief Each action's records, by action id; actions whose result holds none are absent.
238 std::map<std::string, std::vector<ActionRecord>, std::less<>> actionRecords;
239 /// @brief The ids of the actions holding each record, by record name, in id order.
240 std::map<std::string, std::vector<std::string>, std::less<>> holding;
241
242 /// @brief See `morph::effects::effectsOf`.
243 /// @param actionId The action's id.
244 /// @return Its effects, or `nullopt` when it declared none.
245 [[nodiscard]] std::optional<std::vector<Effect>> effectsOf(std::string_view actionId) const {
246 if (auto const found = effects.find(actionId); found != effects.end()) {
247 return found->second;
248 }
249 return std::nullopt;
250 }
251
252 /// @brief See `morph::effects::recordsOf`.
253 /// @param actionId The action's id.
254 /// @return The records its result holds.
255 [[nodiscard]] std::vector<ActionRecord> recordsOf(std::string_view actionId) const {
256 if (auto const found = actionRecords.find(actionId); found != actionRecords.end()) {
257 return found->second;
258 }
259 return {};
260 }
261
262 /// @brief See `morph::effects::recordNamed`.
263 /// @param name A record's wire name.
264 /// @return The record, or null.
265 [[nodiscard]] record::RecordInfo const* recordNamed(std::string_view name) const {
266 auto const found = std::ranges::find(records, name, &record::RecordInfo::name);
267 return found == records.end() ? nullptr : &*found;
268 }
269
270 /// @brief See `morph::effects::actionsHolding`.
271 /// @param record A record's wire name.
272 /// @return The ids of the actions whose result holds it.
273 [[nodiscard]] std::vector<std::string> actionsHolding(std::string_view record) const {
274 if (auto const found = holding.find(record); found != holding.end()) {
275 return found->second;
276 }
277 return {};
278 }
279};
280
281/// @brief Whether @p prefix is @p path or a JSON path that contains it.
282/// @param prefix A shorter path; `""` contains every path.
283/// @param path The path.
284/// @return True when @p path is at or below @p prefix.
285[[nodiscard]] inline bool pathWithin(std::string_view prefix, std::string_view path) {
286 if (prefix.empty() || prefix == path) {
287 return true;
288 }
289 return path.size() > prefix.size() && path.starts_with(prefix) && path[prefix.size()] == '.';
290}
291
292/// @brief Whether two registrations of one record type declared the same record.
293/// @param lhs One registration.
294/// @param rhs The other.
295/// @return True when name, key, sources and privacy all match.
296[[nodiscard]] inline bool sameRegistration(record::RecordInfo const& lhs, record::RecordInfo const& rhs) {
297 return lhs.name == rhs.name && lhs.type == rhs.type && lhs.keyField == rhs.keyField &&
298 lhs.derivedFrom == rhs.derivedFrom && lhs.privateKeys == rhs.privateKeys &&
299 lhs.keyIsString == rhs.keyIsString;
300}
301
302/// @brief Appends a problem for every `derivedFrom` cycle among the records.
303/// @param records The records, one registration per type.
304/// @param indexOf Each record type's first registration.
305/// @param problems Where to append.
306inline void findDerivationCycles(std::vector<record::RecordInfo> const& records,
307 std::map<std::type_index, std::size_t> const& indexOf,
308 std::vector<std::string>& problems) {
309 enum class Mark : std::uint8_t { Unvisited, OnPath, Done };
310 std::vector<Mark> marks(records.size(), Mark::Unvisited);
311 std::vector<std::size_t> path;
312 auto const visit = [&](auto const& self, std::size_t index) -> void {
313 marks[index] = Mark::OnPath;
314 path.push_back(index);
315 for (auto const& source : records[index].derivedFrom) {
316 auto const found = indexOf.find(source);
317 if (found == indexOf.end()) {
318 continue;
319 }
320 if (marks[found->second] == Mark::OnPath) {
321 std::string chain;
322 auto const start = std::ranges::find(path, found->second);
323 for (auto step = start; step != path.end(); ++step) {
324 chain += records[*step].name + " -> ";
325 }
326 chain += records[found->second].name;
327 problems.push_back("record `" + records[found->second].name + "` is derived from itself: " + chain);
328 } else if (marks[found->second] == Mark::Unvisited) {
329 self(self, found->second);
330 }
331 }
332 path.pop_back();
333 marks[index] = Mark::Done;
334 };
335 // Registration order, so the cycle a message names does not depend on how `type_index` sorts.
336 for (std::size_t index = 0; index < records.size(); ++index) {
337 if (marks[index] == Mark::Unvisited) {
338 visit(visit, index);
339 }
340 }
341}
342
343/// @brief Matches action results against records and validates the configuration.
344/// @param records The records, as registered.
345/// @param resultSlots Every action's result slots, by action id.
346/// @param declared Every action's effects declarations, by action id.
347/// @param slotConflicts Action ids registered by more than one action type.
348/// @return The resolved registries.
349/// @throws morph::model::EffectsRegistrationError listing every problem, when there is one.
350[[nodiscard]] inline Resolved resolve(std::vector<record::RecordInfo> records,
351 model::detail::ActionResultSlotMap const& resultSlots,
352 EffectsMap const& declared, std::vector<std::string> const& slotConflicts) {
353 std::vector<std::string> problems;
354
355 // An identical registration, as a record registered in a header gives once per translation unit, is
356 // one registration. Any other second registration of a type or a name is a conflict.
357 std::vector<record::RecordInfo> distinct;
358 for (auto& info : records) {
359 auto const sameType = std::ranges::find(distinct, info.type, &record::RecordInfo::type);
360 if (sameType != distinct.end()) {
361 if (sameType->name != info.name) {
362 problems.push_back(std::string{"type "} + info.type.name() +
363 " is registered under two record names (`" + sameType->name + "` and `" +
364 info.name + "`)");
365 } else if (!sameRegistration(*sameType, info)) {
366 problems.push_back("record `" + info.name + "` is registered twice with different options");
367 }
368 continue;
369 }
370 auto const sameName = std::ranges::find(distinct, info.name, &record::RecordInfo::name);
371 if (sameName != distinct.end()) {
372 problems.push_back("record name `" + info.name + "` is registered for two types (" +
373 sameName->type.name() + " and " + info.type.name() + ")");
374 continue;
375 }
376 distinct.push_back(std::move(info));
377 }
378 records = std::move(distinct);
379
380 std::map<std::type_index, std::size_t> indexOf;
381 for (std::size_t index = 0; index < records.size(); ++index) {
382 auto const& info = records[index];
383 indexOf.emplace(info.type, index);
384 if (info.keyField && !info.derivedFrom.empty()) {
385 problems.push_back("record `" + info.name +
386 "` is derived from other records but has a key; a derived record is keyless");
387 }
388 }
389 // A source that is no record never changes, so the derived record would never be refreshed: refused, not ignored.
390 for (auto const& info : records) {
391 for (auto const& source : info.derivedFrom) {
392 if (!indexOf.contains(source)) {
393 problems.push_back("record `" + info.name + "` is derived from " + source.name() +
394 ", which is not a registered record");
395 }
396 }
397 }
398 findDerivationCycles(records, indexOf, problems);
399
400 for (auto const& id : slotConflicts) {
401 problems.push_back("action `" + id + "` is registered by two action types; give each its own id");
402 }
403
405 for (auto const& [actionId, result] : resultSlots) {
406 std::vector<model::detail::ResultSlot const*> sorted;
407 sorted.reserve(result.resultSlots.size());
408 for (auto const& slot : result.resultSlots) {
409 sorted.push_back(&slot);
410 }
411 // A prefix sorts before every path below it, so a record is always kept before what it contains.
412 std::ranges::stable_sort(sorted, std::less<>{}, [](auto const* slot) { return std::string_view{slot->path}; });
413 std::vector<std::string_view> kept;
414 std::vector<ActionRecord> matched;
415 for (auto const* slot : sorted) {
416 auto const found = indexOf.find(slot->type);
417 if (found == indexOf.end()) {
418 continue;
419 }
420 if (std::ranges::any_of(kept, [&](std::string_view prefix) { return pathWithin(prefix, slot->path); })) {
421 continue;
422 }
423 kept.emplace_back(slot->path);
424 auto const& info = records[found->second];
425 auto entry = std::ranges::find(matched, info.name, &ActionRecord::record);
426 if (entry == matched.end()) {
427 matched.push_back(ActionRecord{.record = info.name, .keyField = info.keyField, .paths = {}});
428 entry = std::prev(matched.end());
429 }
430 entry->paths.push_back(slot->path);
431 }
432 if (!matched.empty()) {
433 for (auto const& entry : matched) {
434 resolved.holding[entry.record].push_back(actionId);
435 }
436 resolved.actionRecords.emplace(actionId, std::move(matched));
437 }
438 }
439
440 for (auto const& [actionId, declarations] : declared) {
441 if (declarations.size() > 1) {
442 problems.push_back("action `" + actionId + "` declares different effects in two registrations");
443 continue;
444 }
445 auto const& declaration = declarations.front();
446 if (declaration.size() > 1 &&
447 std::ranges::any_of(declaration, [](DeclaredEffect const& effect) { return effect.kind == Kind::None; })) {
448 problems.push_back("action `" + actionId + "` declares none together with other effects");
449 continue;
450 }
451 std::vector<Effect> effects;
452 for (auto const& effect : declaration) {
453 if (!effect.record) {
454 effects.push_back(Effect{.kind = effect.kind, .record = {}});
455 continue;
456 }
457 char const* const kindName = effect.kind == Kind::Updates ? "updates" : "changes";
458 auto const found = indexOf.find(*effect.record);
459 if (found == indexOf.end()) {
460 problems.push_back("action `" + actionId + "` declares " + kindName + "<" + effect.record->name() +
461 ">, which is not a registered record");
462 continue;
463 }
464 auto const& info = records[found->second];
465 if (effect.kind == Kind::Updates) {
466 if (!info.keyField) {
467 problems.push_back("action `" + actionId + "` declares updates on record `" + info.name +
468 "`, which has no key");
469 }
470 auto const held = resolved.actionRecords.find(actionId);
471 if (held == resolved.actionRecords.end() ||
472 std::ranges::find(held->second, info.name, &ActionRecord::record) == held->second.end()) {
473 problems.push_back("action `" + actionId + "` declares updates on record `" + info.name +
474 "`, but its result holds no `" + info.name + "`");
475 }
476 }
477 effects.push_back(Effect{.kind = effect.kind, .record = info.name});
478 }
479 resolved.effects.emplace(actionId, std::move(effects));
480 }
481
482 if (!problems.empty()) {
483 throw model::EffectsRegistrationError(std::move(problems));
484 }
485 resolved.records = std::move(records);
486 return resolved;
487}
488
489/// @brief Resolves a registry built by a test, leaving the process registry untouched.
490/// @param records The records.
491/// @param resultSlots The action result slots, by action id.
492/// @param declared The effects declarations, by action id.
493/// @param slotConflicts Action ids registered by more than one action type.
494/// @return The resolved registries.
495/// @throws morph::model::EffectsRegistrationError listing every problem, when there is one.
496[[nodiscard]] inline Resolved resolveForTesting(std::vector<record::RecordInfo> records,
497 model::detail::ActionResultSlotMap const& resultSlots,
498 EffectsMap const& declared,
499 std::vector<std::string> const& slotConflicts = {}) {
500 return resolve(std::move(records), resultSlots, declared, slotConflicts);
501}
502
503/// @brief The process registries, matched and validated on the first call.
504///
505/// Every call is a read of a process-level registry and closes the
506/// registration latch in a debug build. The first call resolves, under
507/// `std::call_once`; an invalid configuration's error is kept and thrown again
508/// on every call.
509/// @return The resolved registries.
510/// @throws morph::model::EffectsRegistrationError when the registered configuration is invalid.
511[[nodiscard]] inline Resolved const& resolved() {
512 model::detail::noteRegistryRead(true);
513 struct State {
514 std::once_flag once;
515 std::optional<Resolved> value;
516 std::optional<model::EffectsRegistrationError> error;
517 };
518 static State state;
519 std::call_once(state.once, [] {
520 try {
521 state.value.emplace(resolve(record::detail::recordRegistry(), model::detail::actionResultSlots(),
522 mutableEffectsRegistry(), model::detail::actionResultConflicts()));
523 } catch (model::EffectsRegistrationError const& error) {
524 state.error.emplace(error);
525 }
526 });
527 if (state.error) {
528 throw *state.error;
529 }
530 return *state.value;
531}
532
533} // namespace detail
534
535/// @brief The effects an action declared.
536/// @param actionId The action's id.
537/// @return Its effects, or `nullopt` when it declared none: the action reads.
538/// @throws morph::model::EffectsRegistrationError when the registered configuration is invalid.
539[[nodiscard]] inline std::optional<std::vector<Effect>> effectsOf(std::string_view actionId) {
540 return detail::resolved().effectsOf(actionId);
541}
542
543/// @brief The records an action's result holds, with the paths to each.
544///
545/// Each path stops at the first registered record on it, so the members of a
546/// record are not searched for further records.
547/// @param actionId The action's id.
548/// @return One entry per record, in the order their first path sorts; empty for an unknown action.
549/// @throws morph::model::EffectsRegistrationError when the registered configuration is invalid.
550[[nodiscard]] inline std::vector<ActionRecord> recordsOf(std::string_view actionId) {
551 return detail::resolved().recordsOf(actionId);
552}
553
554/// @brief A registered record, by wire name.
555/// @param name The record's wire name.
556/// @return The record, valid for the life of the process, or null when no record has that name.
557/// @throws morph::model::EffectsRegistrationError when the registered configuration is invalid.
558[[nodiscard]] inline record::RecordInfo const* recordNamed(std::string_view name) {
559 return detail::resolved().recordNamed(name);
560}
561
562/// @brief The actions whose result holds a record: those whose `recordsOf` names it.
563/// @param record The record's wire name.
564/// @return The action ids, in id order; empty for a record no result holds.
565/// @throws morph::model::EffectsRegistrationError when the registered configuration is invalid.
566[[nodiscard]] inline std::vector<std::string> actionsHolding(std::string_view record) {
567 return detail::resolved().actionsHolding(record);
568}
569
570} // namespace morph::effects
571
572/// @def BRIDGE_REGISTER_EFFECTS
573/// @brief Declares what action @p A changes: any of `morph::effects::updates<R>`, `changes<R>`,
574/// `changesInstance` and `none`.
575///
576/// Place it beside the action's registration, where `ActionTraits<A>` is visible. It defines no
577/// specialisation, so a translation unit that does not see it is consistent with one that does. Repeating
578/// the same effects is a no-op; different effects for one action are refused on the first read. Expands to
579/// nothing under `MORPH_CLIENT_ONLY`.
580/// @param A The action type.
581/// @param ... The effects.
582// Each translation unit that includes a registration registers its own copy, which is the point, so the unnamed
583// namespace in a header and its static initialiser are suppressed here, once, rather than at every use.
584// NOLINTBEGIN(bugprone-throwing-static-initialization,cert-err58-cpp,misc-anonymous-namespace-in-header,cert-dcl59-cpp)
585#ifdef MORPH_CLIENT_ONLY
586#define BRIDGE_REGISTER_EFFECTS(A, ...)
587#else
588#define BRIDGE_REGISTER_EFFECTS(A, ...) \
589 namespace { \
590 [[maybe_unused]] const bool BRIDGE_DETAIL_CAT(bridge_effects_reg_, __COUNTER__) = \
591 morph::effects::detail::registerEffectsOnce<A, __VA_ARGS__>(); \
592 }
593#endif
594// NOLINTEND(bugprone-throwing-static-initialization,cert-err58-cpp,misc-anonymous-namespace-in-header,cert-dcl59-cpp)
Resolved const & resolved()
The process registries, matched and validated on the first call.
Definition effects.hpp:511
std::optional< std::vector< Effect > > effectsOf(std::string_view actionId)
The effects an action declared.
Definition effects.hpp:539
bool sameRegistration(record::RecordInfo const &lhs, record::RecordInfo const &rhs)
Whether two registrations of one record type declared the same record.
Definition effects.hpp:296
Resolved resolveForTesting(std::vector< record::RecordInfo > records, model::detail::ActionResultSlotMap const &resultSlots, EffectsMap const &declared, std::vector< std::string > const &slotConflicts={})
Resolves a registry built by a test, leaving the process registry untouched.
Definition effects.hpp:496
void addEffects(EffectsMap &map, std::string_view actionId, std::vector< DeclaredEffect > declared)
Adds one declaration for an action, unless an equal one is already there.
Definition effects.hpp:186
bool registerEffectsOnce() noexcept
Static-init helper for BRIDGE_REGISTER_EFFECTS.
Definition effects.hpp:218
record::RecordInfo const * recordNamed(std::string_view name)
A registered record, by wire name.
Definition effects.hpp:558
void findDerivationCycles(std::vector< record::RecordInfo > const &records, std::map< std::type_index, std::size_t > const &indexOf, std::vector< std::string > &problems)
Appends a problem for every derivedFrom cycle among the records.
Definition effects.hpp:306
Resolved resolve(std::vector< record::RecordInfo > records, model::detail::ActionResultSlotMap const &resultSlots, EffectsMap const &declared, std::vector< std::string > const &slotConflicts)
Matches action results against records and validates the configuration.
Definition effects.hpp:350
std::map< std::string, std::vector< std::vector< DeclaredEffect > >, std::less<> > EffectsMap
Every distinct effects declaration, by action id. More than one for an id is a conflict.
Definition effects.hpp:144
Kind
The kind of one declared effect.
Definition effects.hpp:90
@ Updates
updates<R>
Definition effects.hpp:91
@ ChangesInstance
changesInstance
Definition effects.hpp:93
bool pathWithin(std::string_view prefix, std::string_view path)
Whether prefix is path or a JSON path that contains it.
Definition effects.hpp:285
EffectsMap & mutableEffectsRegistry()
The process-level effects registry, appended to by static registrars.
Definition effects.hpp:208
std::vector< ActionRecord > recordsOf(std::string_view actionId)
The records an action's result holds, with the paths to each.
Definition effects.hpp:550
std::vector< std::string > actionsHolding(std::string_view record)
The actions whose result holds a record: those whose recordsOf names it.
Definition effects.hpp:566
Where an action's result holds which reflected types.
A record an action's result holds, and where.
Definition effects.hpp:112
std::vector< std::string > paths
The JSON paths at which the result holds the record; "" is the result itself.
Definition effects.hpp:118
friend bool operator==(ActionRecord const &lhs, ActionRecord const &rhs)=default
Memberwise equality.
std::optional< std::string > keyField
The record's key field, empty for a keyless record.
Definition effects.hpp:116
std::string record
The record's wire name.
Definition effects.hpp:114
What one successful execute changed.
One effect of an action, with its record resolved to a wire name.
Definition effects.hpp:98
Kind kind
What the effect is.
Definition effects.hpp:100
std::string record
The record's wire name; empty for ChangesInstance and None.
Definition effects.hpp:102
friend bool operator==(Effect const &lhs, Effect const &rhs)=default
Memberwise equality.
Effect: the state of the instance the action ran on changed.
Definition effects.hpp:84
Effect: the action may create, remove or change rows of record R, which its result need not carry.
Definition effects.hpp:81
One effect as a registrar records it: the record still a type, not yet a name.
Definition effects.hpp:130
friend bool operator==(DeclaredEffect const &lhs, DeclaredEffect const &rhs)=default
Memberwise equality.
std::optional< std::type_index > record
The record type, for Updates and Changes.
Definition effects.hpp:134
Maps an effect tag type to its DeclaredEffect.
Definition effects.hpp:149
The matched and validated registries: what every read is answered from.
Definition effects.hpp:232
std::vector< ActionRecord > recordsOf(std::string_view actionId) const
See morph::effects::recordsOf.
Definition effects.hpp:255
std::vector< record::RecordInfo > records
The records, one entry per registered type, in registration order.
Definition effects.hpp:234
record::RecordInfo const * recordNamed(std::string_view name) const
See morph::effects::recordNamed.
Definition effects.hpp:265
std::map< std::string, std::vector< ActionRecord >, std::less<> > actionRecords
Each action's records, by action id; actions whose result holds none are absent.
Definition effects.hpp:238
std::vector< std::string > actionsHolding(std::string_view record) const
See morph::effects::actionsHolding.
Definition effects.hpp:273
std::map< std::string, std::vector< std::string >, std::less<> > holding
The ids of the actions holding each record, by record name, in id order.
Definition effects.hpp:240
std::map< std::string, std::vector< Effect >, std::less<> > effects
Each action's effects, by action id; actions that declared none are absent.
Definition effects.hpp:236
std::optional< std::vector< Effect > > effectsOf(std::string_view actionId) const
See morph::effects::effectsOf.
Definition effects.hpp:245
Effect: the action changes nothing, declared explicitly.
Definition effects.hpp:87
Effect: the action edits existing rows of record R, and its result carries them.
Definition effects.hpp:76
Traits specialisation that maps an action type to its id, JSON codec, and result type.
Definition registry.hpp:48
Thrown on a read of the effects or record registry when the registered configuration is invalid.
Definition effects.hpp:46
EffectsRegistrationError(std::vector< std::string > problems)
Constructs the error from the problems found.
Definition effects.hpp:49
std::vector< std::string > const & problems() const noexcept
The problems found, in the order they were found.
Definition effects.hpp:54
What one BRIDGE_REGISTER_RECORD declared.
Definition records.hpp:48
std::vector< std::type_index > derivedFrom
The source types this record is derived from.
Definition records.hpp:56
std::type_index type
The record's C++ type; records and effects are matched by this identity.
Definition records.hpp:52
bool keyIsString
True when the key's raw type is std::string; false when it is integral or there is no key.
Definition records.hpp:60
std::string name
The wire name of the record type.
Definition records.hpp:50
bool privateKeys
True when the record's keys must not reach other principals.
Definition records.hpp:58
std::optional< std::string > keyField
The glaze JSON name of the key member; empty for a keyless record or one without an id member.
Definition records.hpp:54