24#include "registry.hpp"
38namespace morph::model {
54 [[nodiscard]] std::vector<std::string>
const&
problems() const noexcept {
return _problems; }
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) {
66 std::vector<std::string> _problems;
71namespace morph::effects {
90enum class Kind : std::uint8_t {
144using EffectsMap = std::map<std::string, std::vector<std::vector<DeclaredEffect>>, std::less<>>;
150 static_assert(
sizeof(E) == 0,
151 "BRIDGE_REGISTER_EFFECTS: an effect is updates<R>, changes<R>, changesInstance or none");
163 static DeclaredEffect value() {
return {.kind = Kind::Changes, .record = std::type_index{
typeid(R)}}; }
169 static DeclaredEffect value() {
return {.kind = Kind::ChangesInstance, .record = std::nullopt}; }
175 static DeclaredEffect value() {
return {.kind = Kind::None, .record = std::nullopt}; }
187 auto entry = map.find(actionId);
188 if (entry == map.end()) {
189 entry = map.emplace(std::string{actionId}, std::vector<std::vector<DeclaredEffect>>{}).first;
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));
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);
202 declarations.push_back(std::move(distinct));
217template <
typename Action,
typename... E>
219 static_assert(
sizeof...(E) > 0,
"BRIDGE_REGISTER_EFFECTS: declare at least one effect");
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.");
236 std::map<std::string, std::vector<Effect>, std::less<>>
effects;
238 std::map<std::string, std::vector<ActionRecord>, std::less<>>
actionRecords;
240 std::map<std::string, std::vector<std::string>, std::less<>>
holding;
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;
255 [[nodiscard]] std::vector<ActionRecord>
recordsOf(std::string_view actionId)
const {
257 return found->second;
267 return found ==
records.end() ? nullptr : &*found;
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;
285[[nodiscard]]
inline bool pathWithin(std::string_view prefix, std::string_view path) {
286 if (prefix.empty() || prefix == path) {
289 return path.size() > prefix.size() && path.starts_with(prefix) && path[prefix.size()] ==
'.';
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()) {
320 if (marks[found->second] == Mark::OnPath) {
322 auto const start = std::ranges::find(path, found->second);
323 for (
auto step = start; step != path.end(); ++step) {
324 chain += records[*step].name +
" -> ";
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);
333 marks[index] = Mark::Done;
336 for (std::size_t index = 0; index < records.size(); ++index) {
337 if (marks[index] == Mark::Unvisited) {
351 model::detail::ActionResultSlotMap
const& resultSlots,
352 EffectsMap const& declared, std::vector<std::string>
const& slotConflicts) {
353 std::vector<std::string> problems;
357 std::vector<record::RecordInfo> distinct;
358 for (
auto& info : records) {
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 `" +
366 problems.push_back(
"record `" + info.name +
"` is registered twice with different options");
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() +
")");
376 distinct.push_back(std::move(info));
378 records = std::move(distinct);
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");
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");
400 for (
auto const&
id : slotConflicts) {
401 problems.push_back(
"action `" +
id +
"` is registered by two action types; give each its own id");
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);
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()) {
420 if (std::ranges::any_of(kept, [&](std::string_view prefix) {
return pathWithin(prefix, slot->path); })) {
423 kept.emplace_back(slot->path);
424 auto const& info = records[found->second];
426 if (entry == matched.end()) {
427 matched.push_back(
ActionRecord{.record = info.name, .keyField = info.keyField, .paths = {}});
428 entry = std::prev(matched.end());
430 entry->paths.push_back(slot->path);
432 if (!matched.empty()) {
433 for (
auto const& entry : matched) {
434 resolved.holding[entry.record].push_back(actionId);
436 resolved.actionRecords.emplace(actionId, std::move(matched));
440 for (
auto const& [actionId, declarations] : declared) {
441 if (declarations.size() > 1) {
442 problems.push_back(
"action `" + actionId +
"` declares different effects in two registrations");
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");
451 std::vector<Effect> effects;
452 for (
auto const& effect : declaration) {
454 effects.push_back(
Effect{.kind = effect.
kind, .record = {}});
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");
464 auto const& info = records[found->second];
466 if (!info.keyField) {
467 problems.push_back(
"action `" + actionId +
"` declares updates on record `" + info.name +
468 "`, which has no key");
470 auto const held =
resolved.actionRecords.find(actionId);
471 if (held ==
resolved.actionRecords.end() ||
473 problems.push_back(
"action `" + actionId +
"` declares updates on record `" + info.name +
474 "`, but its result holds no `" + info.name +
"`");
477 effects.push_back(
Effect{.kind = effect.
kind, .record = info.name});
479 resolved.effects.emplace(actionId, std::move(effects));
482 if (!problems.empty()) {
485 resolved.records = std::move(records);
497 model::detail::ActionResultSlotMap
const& resultSlots,
499 std::vector<std::string>
const& slotConflicts = {}) {
500 return resolve(std::move(records), resultSlots, declared, slotConflicts);
512 model::detail::noteRegistryRead(
true);
515 std::optional<Resolved> value;
516 std::optional<model::EffectsRegistrationError> error;
519 std::call_once(state.once, [] {
521 state.value.emplace(resolve(record::detail::recordRegistry(), model::detail::actionResultSlots(),
522 mutableEffectsRegistry(), model::detail::actionResultConflicts()));
524 state.error.emplace(error);
539[[nodiscard]]
inline std::optional<std::vector<Effect>>
effectsOf(std::string_view actionId) {
550[[nodiscard]]
inline std::vector<ActionRecord>
recordsOf(std::string_view actionId) {
566[[nodiscard]]
inline std::vector<std::string>
actionsHolding(std::string_view record) {
585#ifdef MORPH_CLIENT_ONLY
586#define BRIDGE_REGISTER_EFFECTS(A, ...)
588#define BRIDGE_REGISTER_EFFECTS(A, ...) \
590 [[maybe_unused]] const bool BRIDGE_DETAIL_CAT(bridge_effects_reg_, __COUNTER__) = \
591 morph::effects::detail::registerEffectsOnce<A, __VA_ARGS__>(); \
Resolved const & resolved()
The process registries, matched and validated on the first call.
std::optional< std::vector< Effect > > effectsOf(std::string_view actionId)
The effects an action declared.
bool sameRegistration(record::RecordInfo const &lhs, record::RecordInfo const &rhs)
Whether two registrations of one record type declared the same record.
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.
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.
bool registerEffectsOnce() noexcept
Static-init helper for BRIDGE_REGISTER_EFFECTS.
record::RecordInfo const * recordNamed(std::string_view name)
A registered record, by wire name.
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.
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.
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.
Kind
The kind of one declared effect.
@ ChangesInstance
changesInstance
bool pathWithin(std::string_view prefix, std::string_view path)
Whether prefix is path or a JSON path that contains it.
EffectsMap & mutableEffectsRegistry()
The process-level effects registry, appended to by static registrars.
std::vector< ActionRecord > recordsOf(std::string_view actionId)
The records an action's result holds, with the paths to each.
std::vector< std::string > actionsHolding(std::string_view record)
The actions whose result holds a record: those whose recordsOf names it.
Where an action's result holds which reflected types.
A record an action's result holds, and where.
std::vector< std::string > paths
The JSON paths at which the result holds the record; "" is the result itself.
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.
std::string record
The record's wire name.
What one successful execute changed.
One effect of an action, with its record resolved to a wire name.
Kind kind
What the effect is.
std::string record
The record's wire name; empty for ChangesInstance and None.
friend bool operator==(Effect const &lhs, Effect const &rhs)=default
Memberwise equality.
Effect: the state of the instance the action ran on changed.
Effect: the action may create, remove or change rows of record R, which its result need not carry.
One effect as a registrar records it: the record still a type, not yet a name.
Kind kind
What the effect is.
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.
Maps an effect tag type to its DeclaredEffect.
The matched and validated registries: what every read is answered from.
std::vector< ActionRecord > recordsOf(std::string_view actionId) const
See morph::effects::recordsOf.
std::vector< record::RecordInfo > records
The records, one entry per registered type, in registration order.
record::RecordInfo const * recordNamed(std::string_view name) const
See morph::effects::recordNamed.
std::map< std::string, std::vector< ActionRecord >, std::less<> > actionRecords
Each action's records, by action id; actions whose result holds none are absent.
std::vector< std::string > actionsHolding(std::string_view record) const
See morph::effects::actionsHolding.
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.
std::map< std::string, std::vector< Effect >, std::less<> > effects
Each action's effects, by action id; actions that declared none are absent.
std::optional< std::vector< Effect > > effectsOf(std::string_view actionId) const
See morph::effects::effectsOf.
Effect: the action changes nothing, declared explicitly.
Effect: the action edits existing rows of record R, and its result carries them.
Traits specialisation that maps an action type to its id, JSON codec, and result type.
Thrown on a read of the effects or record registry when the registered configuration is invalid.
EffectsRegistrationError(std::vector< std::string > problems)
Constructs the error from the problems found.
std::vector< std::string > const & problems() const noexcept
The problems found, in the order they were found.
What one BRIDGE_REGISTER_RECORD declared.
std::vector< std::type_index > derivedFrom
The source types this record is derived from.
std::type_index type
The record's C++ type; records and effects are matched by this identity.
bool keyIsString
True when the key's raw type is std::string; false when it is integral or there is no key.
std::string name
The wire name of the record type.
bool privateKeys
True when the record's keys must not reach other principals.
std::optional< std::string > keyField
The glaze JSON name of the key member; empty for a keyless record or one without an id member.