12#include <glaze/glaze.hpp>
21#include <unordered_map>
25#include "../forms/field_errors.hpp"
32namespace morph::model {
39template <
typename Model>
47template <
typename Action>
77[[nodiscard]]
inline std::atomic<bool>& registrationPhaseFlag() noexcept {
78 static std::atomic<bool> closed{
false};
101inline void closeRegistrationPhase() noexcept {
102 detail::registrationPhaseFlag().store(
true, std::memory_order_release);
111[[nodiscard]]
inline bool registrationPhaseClosed() noexcept {
112 return detail::registrationPhaseFlag().load(std::memory_order_acquire);
123inline void reopenRegistrationPhaseForTesting() noexcept {
124 registrationPhaseFlag().store(
false, std::memory_order_release);
139inline void noteRegistryRead([[maybe_unused]]
bool isProcessRegistry)
noexcept {
141 if (isProcessRegistry) {
142 ::morph::model::closeRegistrationPhase();
156using PairKeyView = std::pair<std::string_view, std::string_view>;
174 using is_transparent = void;
179 [[nodiscard]] std::size_t operator()(PairKeyView key)
const noexcept {
180 std::size_t seed = std::hash<std::string_view>{}(key.first);
182 seed ^= std::hash<std::string_view>{}(key.second) + 0x9e3779b9 + (seed << 6) + (seed >> 2);
189 [[nodiscard]] std::size_t operator()(
const std::pair<std::string, std::string>& key)
const noexcept {
190 return (*
this)(PairKeyView{key.first, key.second});
201 using is_transparent = void;
209 template <
typename LhsT,
typename RhsT>
210 [[nodiscard]]
bool operator()(
const LhsT& lhs,
const RhsT& rhs)
const noexcept {
211 return std::string_view{lhs.first} == std::string_view{rhs.first} &&
212 std::string_view{lhs.second} == std::string_view{rhs.second};
222concept HasValidate =
requires(
const A& act) {
223 { act.validate() } -> std::convertible_to<bool>;
260template <
typename Action>
269 static constexpr bool ready(
const Action& action) {
270 if constexpr (detail::HasValidate<Action>) {
271 return action.validate();
293 std::type_index incoming)
294 : std::logic_error(
"morph: the " + std::string{what} +
" id `" + std::string{taken} +
295 "` is registered for two types (" + held.name() +
" and " + incoming.name() +
296 "); give each type its own id") {}
307template <
typename Map,
typename Key>
309void claimId(Map& types, std::string_view what, Key
const& key, std::string_view name, std::type_index type) {
310 auto const [iter, inserted] = types.try_emplace(key, type);
311 if (!inserted && iter->second != type) {
312 throw DuplicateRegistrationError{what, name, iter->second, type};
318[[noreturn]]
inline void abortRegistration(DuplicateRegistrationError
const& error)
noexcept {
319 static_cast<void>(std::fputs(error.what(), stderr));
320 static_cast<void>(std::fputc(
'\n', stderr));
351 : std::runtime_error(
"action failed validation: " + std::string{modelType} +
"/" + std::string{actionType}) {}
394 : std::runtime_error(
"action executed but was not recorded: " +
cause),
395 _result(std::move(
result)),
396 _cause(std::move(
cause)) {}
400 [[nodiscard]]
const std::string&
result() const noexcept {
return _result; }
404 [[nodiscard]]
const std::string&
cause() const noexcept {
return _cause; }
415enum class Loggable : std::uint8_t { No, Yes };
427template <
typename Action>
444concept HasLoggableFlag =
requires {
454constexpr Loggable actionLoggable() {
455 if constexpr (HasLoggableFlag<A>) {
456 return ActionTraits<A>::loggable;
458 return Loggable::Yes;
468concept HasPayloadSchema =
requires {
469 { ActionTraits<A>::payloadSchema() } -> std::convertible_to<std::string_view>;
494inline std::string actionPayloadSchema() {
495 if constexpr (HasPayloadSchema<A>) {
496 return std::string{ActionTraits<A>::payloadSchema()};
506[[nodiscard]]
inline std::string_view currentRequestId() noexcept {
507 auto const*
const ctx = ::morph::session::current();
508 return ctx !=
nullptr ? std::string_view{ctx->requestId} : std::string_view{};
531inline void recordActionSuccess(IModelHolder& holder, std::string modelType, std::string actionType,
532 std::string payload, std::string schema, std::string result) {
534 holder.recordIfAttached(::morph::journal::LogEntry{
536 .modelType = std::move(modelType),
538 .actionType = std::move(actionType),
539 .payload = std::move(payload),
540 .schema = std::move(schema),
541 .result = std::move(result),
542 .outcome = ::morph::journal::Outcome::Succeeded,
558inline void recordActionFailure(IModelHolder& holder, std::string modelType, std::string actionType,
559 std::string payload, std::string schema, std::string error) {
561 holder.recordIfAttached(::morph::journal::LogEntry{
563 .modelType = std::move(modelType),
565 .actionType = std::move(actionType),
566 .payload = std::move(payload),
567 .schema = std::move(schema),
569 .outcome = ::morph::journal::Outcome::Failed,
570 .error = std::move(error),
577struct ParseError : std::runtime_error {
578 using std::runtime_error::runtime_error;
601struct EscapingWriteOpts : glz::opts {
604 bool escape_control_characters =
true;
615struct ActionDescription {
626 std::vector<std::string> required;
640[[nodiscard]]
inline ActionDescription buildActionDescription() {
641 ActionDescription desc;
642 desc.schema = ::morph::forms::schemaJson<A>();
646 glz::generic_u64 dom{};
647 if (glz::read_json(dom, desc.schema) || !dom.is_object()) {
650 if (dom.contains(
"required") && dom[
"required"].is_array()) {
651 for (
const auto& name : dom[
"required"].get_array()) {
652 if (name.is_string()) {
653 desc.required.push_back(name.template get<std::string>());
657 dom[
"x-payloadFingerprint"] = payloadFingerprint<A>();
658 dom[
"x-payloadShape"] = payloadShapeString<A>();
682 if (!glz::write_json(dom, merged)) {
683 desc.schema = std::move(merged);
702[[nodiscard]]
inline const ActionDescription& actionDescription() {
703 static const ActionDescription kDesc = buildActionDescription<A>();
708inline class ActionDispatcher& defaultDispatcher();
709inline class ModelRegistryFactory& defaultRegistry();
715class ActionDispatcher {
718 using Runner = std::function<std::string(IModelHolder&, std::string_view)>;
725 std::map<std::string, std::string, std::less<>> errors;
735 using Evaluator = Evaluation (*)(std::string_view);
739 using DispatchDone = std::function<void(std::string, std::exception_ptr)>;
744 using AsyncRunner = void (*)(IModelHolder&, std::string_view,
745 const std::shared_ptr<::morph::exec::detail::TaskResumer>&, ::core::async::StopToken,
786 template <
typename Model,
typename Action>
787 void registerAction(std::string_view modelId, std::string_view actionId) {
788 Key key{std::string{modelId}, std::string{actionId}};
789 detail::claimId(_actionTypes,
"action", key, key.first +
"/" + key.second, std::type_index{typeid(Action)});
790 ActionEntry& entry = _actions[std::move(key)];
791 if constexpr (isTaskHandler<decltype(std::declval<Model&>().execute(std::declval<Action&>()))>) {
792 entry.asyncRunner = &runTaskHandler<Model, Action>;
793 entry.runner = [](IModelHolder&, std::string_view) -> std::string {
794 throw std::logic_error{
795 "ActionDispatcher::dispatch: this action's handler returns core::async::Task and cannot complete "
796 "synchronously; dispatch it with dispatchAsync"};
799 entry.runner = &runHandler<Model, Action>;
801 entry.evaluator = &evaluateAction<Model, Action>;
803 entry.schema = detail::actionPayloadSchema<Action>();
809 entry.describe = []() ->
const ActionDescription& {
return actionDescription<Action>(); };
820 template <
typename Model,
typename Action>
821 static Action prepareAction(std::string_view payloadJson) {
822 MORPH_ZONE(
"ActionDispatcher::prepareAction");
823 auto action = ActionTraits<Action>::fromJson(payloadJson);
830 ::morph::forms::reconcileDeclaredPrecision(action);
837 ::morph::forms::enforceQuantityBounds(action);
846 ::morph::forms::recomputeAll(action);
859 throw ValidationError{ModelTraits<Model>::typeId(), ActionTraits<Action>::typeId()};
870 template <
typename Model,
typename Action>
871 static Evaluation evaluateAction(std::string_view payloadJson) {
872 MORPH_ZONE(
"ActionDispatcher::evaluateAction");
873 auto action = ActionTraits<Action>::fromJson(payloadJson);
874 ::morph::forms::reconcileDeclaredPrecision(action);
875 ::morph::forms::recomputeAll(action);
876 Evaluation evaluation;
877 evaluation.errors = ::morph::forms::fieldErrors(action);
880 evaluation.body = ActionTraits<Action>::toJson(action);
891 template <
typename Model,
typename Action>
892 static std::string runHandler(IModelHolder& holder, std::string_view payloadJson) {
893 auto action = prepareAction<Model, Action>(payloadJson);
894 auto& model = holder.template into<Model>();
910#pragma warning(disable : 4702)
914 return model.execute(action);
915 }
catch (
const std::exception& exc [[maybe_unused]]) {
916 if constexpr (detail::actionLoggable<Action>() == Loggable::Yes) {
917 if (holder.hasActionLog()) {
918 detail::recordActionFailure(holder, std::string{ModelTraits<Model>::typeId()},
919 std::string{ActionTraits<Action>::typeId()},
920 std::string{payloadJson}, detail::actionPayloadSchema<Action>(),
941 std::string resultJson;
943 resultJson = ActionTraits<Action>::resultToJson(result);
944 if constexpr (detail::actionLoggable<Action>() == Loggable::Yes) {
945 if (holder.hasActionLog()) {
947 detail::recordActionSuccess(holder, std::string{ModelTraits<Model>::typeId()},
948 std::string{ActionTraits<Action>::typeId()}, std::string{payloadJson},
949 detail::actionPayloadSchema<Action>(), resultJson);
952 }
catch (
const std::exception& exc) {
953 throw ActionRecordingError{std::move(resultJson), exc.what()};
970 template <
typename Model,
typename Action>
974 static void runTaskHandler(IModelHolder& holder, std::string_view payloadJson,
975 const std::shared_ptr<::morph::exec::detail::TaskResumer>& executor,
976 ::core::async::StopToken token, DispatchDone done) {
977 using R = HandlerResultT<decltype(std::declval<Model&>().execute(std::declval<Action&>()))>;
980 std::shared_ptr<Action> action;
981 ::core::async::Task<R> task;
983 action = std::make_shared<Action>(prepareAction<Model, Action>(payloadJson));
984 task = holder.template into<Model>().execute(*action);
986 done(std::string{}, std::current_exception());
989 detail::startTaskHandler<R>(
990 executor, std::move(task), std::move(token),
991 [&holder, action, payload = std::string{payloadJson}, done = std::move(done)](
992 std::optional<R> result,
const std::exception_ptr& error)
mutable {
994 static_cast<void>(holder);
1001 if constexpr (detail::actionLoggable<Action>() == Loggable::Yes) {
1002 if (holder.hasActionLog()) {
1004 std::rethrow_exception(error);
1005 }
catch (
const std::exception& exc) {
1006 detail::recordActionFailure(holder, std::string{ModelTraits<Model>::typeId()},
1007 std::string{ActionTraits<Action>::typeId()}, payload,
1008 detail::actionPayloadSchema<Action>(), exc.what());
1017 done(std::string{}, error);
1023 std::string resultJson;
1025 resultJson = ActionTraits<Action>::resultToJson(*result);
1026 if constexpr (detail::actionLoggable<Action>() == Loggable::Yes) {
1027 if (holder.hasActionLog()) {
1028 detail::recordActionSuccess(holder, std::string{ModelTraits<Model>::typeId()},
1029 std::string{ActionTraits<Action>::typeId()}, payload,
1030 detail::actionPayloadSchema<Action>(), resultJson);
1033 }
catch (
const std::exception& exc) {
1035 std::make_exception_ptr(ActionRecordingError{std::move(resultJson), exc.what()}));
1038 done(std::string{}, std::current_exception());
1041 done(std::move(resultJson),
nullptr);
1047 std::string dispatch(std::string_view modelId, std::string_view actionId, IModelHolder& holder,
1048 std::string_view payload) {
1055 detail::noteRegistryRead(
this == &defaultDispatcher());
1060 auto iter = _actions.find(detail::PairKeyView{modelId, actionId});
1061 if (iter == _actions.end()) {
1065 throw std::runtime_error(
"unknown action: " + std::string{modelId} +
"/" + std::string{actionId});
1067 return iter->second.runner(holder, payload);
1081 [[nodiscard]] Evaluation evaluate(std::string_view modelId, std::string_view actionId,
1082 std::string_view payload)
const {
1084 detail::noteRegistryRead(
this == &defaultDispatcher());
1085 auto iter = _actions.find(detail::PairKeyView{modelId, actionId});
1086 if (iter == _actions.end()) {
1087 throw std::runtime_error(
"unknown action: " + std::string{modelId} +
"/" + std::string{actionId});
1089 return iter->second.evaluator(payload);
1105 void dispatchAsync(std::string_view modelId, std::string_view actionId, IModelHolder& holder,
1106 std::string_view payload,
const std::shared_ptr<::morph::exec::detail::TaskResumer>& executor,
1107 ::core::async::StopToken token, DispatchDone done) {
1108 MORPH_ZONE(
"ActionDispatcher::dispatchAsync");
1110 const ActionEntry* entry =
nullptr;
1112 detail::noteRegistryRead(
this == &defaultDispatcher());
1113 auto iter = _actions.find(detail::PairKeyView{modelId, actionId});
1114 if (iter == _actions.end()) {
1115 throw std::runtime_error(
"unknown action: " + std::string{modelId} +
"/" + std::string{actionId});
1117 entry = &iter->second;
1119 done(std::string{}, std::current_exception());
1122 if (entry->asyncRunner !=
nullptr) {
1123 entry->asyncRunner(holder, payload, executor, std::move(token), std::move(done));
1128 result = entry->runner(holder, payload);
1130 done(std::string{}, std::current_exception());
1133 done(std::move(result),
nullptr);
1144 [[nodiscard]]
bool dispatchesAsync(std::string_view modelId, std::string_view actionId)
const {
1145 auto iter = _actions.find(detail::PairKeyView{modelId, actionId});
1146 return iter != _actions.end() && iter->second.asyncRunner !=
nullptr;
1155 [[nodiscard]]
bool coalesce(std::string_view modelId, std::string_view actionId)
const {
1156 auto iter = _actions.find(detail::PairKeyView{modelId, actionId});
1157 return iter != _actions.end() && iter->second.coalesce;
1174 [[nodiscard]] std::string schemaFor(std::string_view modelId, std::string_view actionId)
const {
1175 auto iter = _actions.find(detail::PairKeyView{modelId, actionId});
1176 return iter == _actions.end() ? std::string{} : iter->second.schema;
1199 [[nodiscard]] std::string schemasJson(std::string_view modelId)
const {
1200 std::vector<std::string_view> actionIds;
1201 for (
const auto& [key, entry] : _actions) {
1202 if (key.first == modelId) {
1203 actionIds.emplace_back(key.second);
1206 std::ranges::sort(actionIds);
1207 std::string out{
"{"};
1209 for (std::string_view
const actionId : actionIds) {
1218 (void)glz::write_json(std::string{actionId}, quoted);
1221 const auto& desc = _actions.find(detail::PairKeyView{modelId, actionId})->second.describe();
1222 out += desc.schema.empty() ?
"{}" : desc.schema;
1241 [[nodiscard]]
const std::vector<std::string>* requiredFieldsFor(std::string_view modelId,
1242 std::string_view actionId)
const {
1243 auto iter = _actions.find(detail::PairKeyView{modelId, actionId});
1244 if (iter == _actions.end()) {
1248 return &iter->second.describe().required;
1249 }
catch (
const std::exception&) {
1263 [[nodiscard]] std::vector<std::string> modelsOf(std::string_view actionId)
const {
1264 std::vector<std::string> models;
1265 for (
const auto& [key, entry] : _actions) {
1266 if (key.second == actionId) {
1267 models.push_back(key.first);
1270 std::ranges::sort(models);
1281 [[nodiscard]] std::vector<std::string> actionsOf(std::string_view modelId)
const {
1282 std::vector<std::string> actions;
1283 for (
const auto& [key, entry] : _actions) {
1284 if (key.first == modelId) {
1285 actions.push_back(key.second);
1288 std::ranges::sort(actions);
1293 static ActionDispatcher& instance() {
return defaultDispatcher(); }
1296 using Key = std::pair<std::string, std::string>;
1303 using Describer = std::function<
const ActionDescription&()>;
1314 struct ActionEntry {
1318 AsyncRunner asyncRunner =
nullptr;
1322 bool coalesce =
false;
1329 std::unordered_map<Key, ActionEntry, PairKeyHash, PairKeyEqual> _actions;
1331 std::unordered_map<Key, std::type_index, PairKeyHash, PairKeyEqual> _actionTypes;
1338class ModelRegistryFactory {
1346 template <
typename Model>
1347 void registerModel(std::string_view modelId) {
1348 detail::claimId(_types,
"model", std::string{modelId}, modelId, std::type_index{
typeid(
Model)});
1349 _factories.insert_or_assign(std::string{modelId}, [] {
return ModelFactory::create<Model>(); });
1385 template <
typename Model,
typename Factory>
1386 requires std::invocable<Factory> &&
1387 std::convertible_to<std::invoke_result_t<Factory>, std::unique_ptr<IModelHolder>>
1388 void registerModel(std::string_view modelId, Factory factory) {
1390 detail::claimId(_types,
"model", std::string{modelId}, modelId, std::type_index{
typeid(
Model)});
1391 _factories.insert_or_assign(std::string{modelId}, [factory = std::move(factory)]()
mutable {
1392 std::unique_ptr<IModelHolder> holder{factory()};
1413 (holder->type() == std::type_index(
typeid(Model)) &&
1414 "registerModel<Model>(modelId, factory): factory returned a holder for a different type"));
1433 std::unique_ptr<IModelHolder> create(std::string_view modelId, std::string_view primary = {}) {
1435 detail::noteRegistryRead(
this == &defaultRegistry());
1436 auto iter = _factories.find(std::string{modelId});
1437 if (iter == _factories.end()) {
1438 throw std::runtime_error(
"unknown model type: " + std::string{modelId});
1440 auto holder = iter->second();
1441 holder->attachIdentity(std::string{primary});
1448 [[nodiscard]]
bool contains(std::string_view modelId)
const {
1449 detail::noteRegistryRead(
this == &defaultRegistry());
1450 return _factories.contains(std::string{modelId});
1454 static ModelRegistryFactory& instance() {
return defaultRegistry(); }
1457 std::unordered_map<std::string, std::function<std::unique_ptr<IModelHolder>()>> _factories;
1459 std::unordered_map<std::string, std::type_index> _types;
1463inline ActionDispatcher& defaultDispatcher() {
1464 static ActionDispatcher inst;
1469inline ModelRegistryFactory& defaultRegistry() {
1470 static ModelRegistryFactory inst;
1486template <
typename Model>
1487inline bool registerModelOnce(std::string_view modelId)
noexcept {
1502 assert(!::morph::model::registrationPhaseClosed() &&
1503 "registerModelOnce: registration after the registration phase closed. The process-level "
1504 "registries are unsynchronised and are read-only once dispatch begins -- registering now "
1505 "races their internals against concurrent lookups (see docs/spec/core/registry.md, "
1506 "\"Thread safety\"). Load and register plugin modules before the first dispatch.");
1508 ModelRegistryFactory::instance().registerModel<
Model>(modelId);
1509 }
catch (DuplicateRegistrationError
const& error) {
1510 detail::abortRegistration(error);
1516template <
typename Model,
typename Action>
1517inline bool registerActionOnce(std::string_view modelId, std::string_view actionId)
noexcept {
1532 assert(!::morph::model::registrationPhaseClosed() &&
1533 "registerActionOnce: registration after the registration phase closed. The process-level "
1534 "registries are unsynchronised and are read-only once dispatch begins -- registering now "
1535 "races their internals against concurrent lookups (see docs/spec/core/registry.md, "
1536 "\"Thread safety\"). Load and register plugin modules before the first dispatch.");
1538 ActionDispatcher::instance().registerAction<
Model,
Action>(modelId, actionId);
1539 }
catch (DuplicateRegistrationError
const& error) {
1540 detail::abortRegistration(error);
1556template <
typename Action>
1557inline bool registerActionResultOnce(std::string_view actionId)
noexcept {
1559 assert(!::morph::model::registrationPhaseClosed() &&
1560 "registerActionResultOnce: registration after the registration phase closed. The process-level "
1561 "registries are unsynchronised and are read-only once dispatch begins -- registering now "
1562 "races their internals against concurrent lookups. Load and register plugin modules before "
1563 "the first dispatch.");
1564 std::type_index
const action{
typeid(
Action)};
1565 auto& entries = mutableActionResultSlots();
1566 if (
auto const existing = entries.find(actionId); existing != entries.end()) {
1567 if (existing->second.action != action) {
1568 auto& conflicts = mutableActionResultConflicts();
1569 if (std::ranges::find(conflicts, actionId) == conflicts.end()) {
1570 conflicts.emplace_back(actionId);
1576 std::string{actionId},
1577 ActionResult{.action = action, .resultSlots = walkResult<typename ActionTraits<Action>::Result>()});
1592template <
typename Model,
typename Action>
1593bool registerActionExecutorOnce(std::string_view modelId, std::string_view actionId)
noexcept;
1610template <
typename Model>
1611void modelSourceRegistrationRequired() {}
1620template <
typename Model,
typename Action>
1621void actionSourceRegistrationRequired() {}
1652#define BRIDGE_DETAIL_CAT_(a, b) a##b
1653#define BRIDGE_DETAIL_CAT(a, b) BRIDGE_DETAIL_CAT_(a, b)
1699#ifdef MORPH_CLIENT_ONLY
1700#define MORPH_DETAIL_REGISTER_MODEL_LOCAL(M, NAME)
1701#define MORPH_DETAIL_REGISTER_ACTION_LOCAL(M, A, NAME)
1702#define MORPH_DETAIL_REGISTER_RESULT_LOCAL(A)
1703#define MORPH_DETAIL_REQUIRE_SOURCE_LOCAL(PREFIX, ...)
1704#define MORPH_DETAIL_DEFINE_SOURCE_LOCAL(...)
1711#define MORPH_DETAIL_REGISTER_MODEL_LOCAL(M, NAME) \
1713 [[maybe_unused]] const bool BRIDGE_DETAIL_CAT(bridge_model_reg_, __COUNTER__) = \
1714 morph::model::detail::registerModelOnce<M>(NAME); \
1716#define MORPH_DETAIL_REGISTER_ACTION_LOCAL(M, A, NAME) \
1718 [[maybe_unused]] const bool BRIDGE_DETAIL_CAT(bridge_action_reg_, __COUNTER__) = \
1719 morph::model::detail::registerActionOnce<M, A>(morph::model::ModelTraits<M>::typeId(), NAME); \
1721#define MORPH_DETAIL_REGISTER_RESULT_LOCAL(A) \
1723 [[maybe_unused]] const bool BRIDGE_DETAIL_CAT(bridge_action_result_reg_, __COUNTER__) = \
1724 morph::model::detail::registerActionResultOnce<A>(morph::model::ActionTraits<A>::typeId()); \
1726#define MORPH_DETAIL_REQUIRE_SOURCE_LOCAL(PREFIX, ...) \
1727 extern template void __VA_ARGS__; \
1729 [[maybe_unused]] const bool BRIDGE_DETAIL_CAT(PREFIX, __COUNTER__) = (__VA_ARGS__, true); \
1731#define MORPH_DETAIL_DEFINE_SOURCE_LOCAL(...) template void __VA_ARGS__;
1742#define BRIDGE_DETAIL_MODEL_TRAITS_ONLY_BODY(M, NAME) \
1744 struct morph::model::ModelTraits<M> { \
1745 static constexpr std::string_view typeId() noexcept { return NAME; } \
1759#define BRIDGE_REGISTER_MODEL(M, NAME) \
1760 BRIDGE_DETAIL_MODEL_TRAITS_ONLY_BODY(M, NAME) \
1761 MORPH_DETAIL_REGISTER_MODEL_LOCAL(M, NAME)
1784#define BRIDGE_DECLARE_MODEL(M, NAME) \
1785 BRIDGE_DETAIL_MODEL_TRAITS_ONLY_BODY(M, NAME) \
1786 MORPH_DETAIL_REQUIRE_SOURCE_LOCAL(bridge_model_src_req_, \
1787 morph::model::detail::modelSourceRegistrationRequired<M>())
1805#define BRIDGE_REGISTER_MODEL_SOURCE(M) \
1806 MORPH_DETAIL_DEFINE_SOURCE_LOCAL(morph::model::detail::modelSourceRegistrationRequired<M>()) \
1807 MORPH_DETAIL_REGISTER_MODEL_LOCAL(M, morph::model::ModelTraits<M>::typeId())
1837#define BRIDGE_REGISTER_ACTION(...) \
1838 BRIDGE_REGISTER_ACTION_PICK(__VA_ARGS__, BRIDGE_REGISTER_ACTION_4, BRIDGE_REGISTER_ACTION_3) \
1892#define BRIDGE_REGISTER_ACTION_FOR_CLIENT(...) \
1893 BRIDGE_REGISTER_ACTION_FOR_CLIENT_PICK(__VA_ARGS__, BRIDGE_REGISTER_ACTION_FOR_CLIENT_5, \
1894 BRIDGE_REGISTER_ACTION_FOR_CLIENT_4) \
1899#define BRIDGE_REGISTER_ACTION_FOR_CLIENT_PICK(_1, _2, _3, _4, _5, NAME, ...) NAME
1902#define BRIDGE_REGISTER_ACTION_FOR_CLIENT_4(M, A, RESULT, NAME) \
1903 BRIDGE_REGISTER_ACTION_FOR_CLIENT_5(M, A, RESULT, NAME, ::morph::model::Loggable::Yes)
1921#define BRIDGE_DETAIL_ACTION_TRAITS_ONLY_BODY(M, A, RESULT_ALIAS, NAME, LOGGABLE) \
1923 struct morph::model::ActionTraits<A> { \
1924 using Result = RESULT_ALIAS; \
1925 static constexpr std::string_view typeId() { return NAME; } \
1926 static constexpr ::morph::model::Loggable loggable = (LOGGABLE); \
1930 static const std::string& payloadSchema() { return ::morph::model::payloadFingerprint<A>(); } \
1931 static std::string toJson(const A& action) { \
1937 if (auto const errCode = glz::write<::morph::model::detail::EscapingWriteOpts{}>(action, out)) { \
1938 throw morph::model::detail::ParseError{glz::format_error(errCode, out)}; \
1942 static A fromJson(std::string_view jsonStr) { \
1947 static constexpr glz::opts kLenientRead{.null_terminated = false, .error_on_unknown_keys = false}; \
1955 ::morph::math::WireClampScope const clampedRationals; \
1956 if (auto const errCode = glz::read<kLenientRead>(action, jsonStr)) { \
1957 throw morph::model::detail::ParseError{glz::format_error(errCode, jsonStr)}; \
1959 if (clampedRationals.clamped() != 0) { \
1960 throw morph::model::detail::ParseError{ \
1961 "action body contains a Rational that cannot be represented exactly"}; \
1965 static std::string resultToJson(const Result& result) { \
1970 if (auto const errCode = glz::write<::morph::model::detail::EscapingWriteOpts{}>(result, out)) { \
1971 throw morph::model::detail::ParseError{glz::format_error(errCode, out)}; \
1975 static Result resultFromJson(std::string_view jsonStr) { \
1978 static constexpr glz::opts kLenientRead{.null_terminated = false, .error_on_unknown_keys = false}; \
1979 if (auto const errCode = glz::read<kLenientRead>(result, jsonStr)) { \
1980 throw morph::model::detail::ParseError{glz::format_error(errCode, jsonStr)}; \
2008#define BRIDGE_DETAIL_ACTION_REGISTRARS(M, A) \
2009 MORPH_DETAIL_REGISTER_ACTION_LOCAL(M, A, morph::model::ActionTraits<A>::typeId()) \
2010 MORPH_DETAIL_REGISTER_RESULT_LOCAL(A) \
2012 [[maybe_unused]] const bool BRIDGE_DETAIL_CAT(bridge_action_exec_reg_, __COUNTER__) = \
2013 morph::model::detail::registerActionExecutorOnce<M, A>(morph::model::ModelTraits<M>::typeId(), \
2014 morph::model::ActionTraits<A>::typeId()); \
2041#define BRIDGE_DETAIL_ACTION_TRAITS_BODY(M, A, RESULT_ALIAS, NAME, LOGGABLE) \
2042 BRIDGE_DETAIL_ACTION_TRAITS_ONLY_BODY(M, A, RESULT_ALIAS, NAME, LOGGABLE) \
2043 BRIDGE_DETAIL_ACTION_REGISTRARS(M, A)
2046#define BRIDGE_REGISTER_ACTION_FOR_CLIENT_5(M, A, RESULT, NAME, LOGGABLE) \
2047 BRIDGE_DETAIL_ACTION_TRAITS_BODY(M, A, RESULT, NAME, LOGGABLE)
2052#define BRIDGE_REGISTER_ACTION_PICK(_1, _2, _3, _4, NAME, ...) NAME
2054#define BRIDGE_REGISTER_ACTION_3(M, A, NAME) BRIDGE_REGISTER_ACTION_4(M, A, NAME, ::morph::model::Loggable::Yes)
2063#define BRIDGE_DETAIL_DEDUCED_ACTION_RESULT(M, A) \
2064 ::morph::model::HandlerResultT<decltype(std::declval<M&>().execute(std::declval<A>()))>
2067#define BRIDGE_REGISTER_ACTION_4(M, A, NAME, LOGGABLE) \
2068 BRIDGE_DETAIL_ACTION_TRAITS_BODY(M, A, BRIDGE_DETAIL_DEDUCED_ACTION_RESULT(M, A), NAME, LOGGABLE)
2091#define BRIDGE_DECLARE_ACTION(...) \
2092 BRIDGE_DECLARE_ACTION_PICK(__VA_ARGS__, BRIDGE_DECLARE_ACTION_4, BRIDGE_DECLARE_ACTION_3) \
2097#define BRIDGE_DECLARE_ACTION_PICK(_1, _2, _3, _4, NAME, ...) NAME
2099#define BRIDGE_DECLARE_ACTION_3(M, A, NAME) BRIDGE_DECLARE_ACTION_4(M, A, NAME, ::morph::model::Loggable::Yes)
2102#define BRIDGE_DECLARE_ACTION_4(M, A, NAME, LOGGABLE) \
2103 BRIDGE_DETAIL_ACTION_TRAITS_ONLY_BODY(M, A, BRIDGE_DETAIL_DEDUCED_ACTION_RESULT(M, A), NAME, LOGGABLE) \
2104 MORPH_DETAIL_REQUIRE_SOURCE_LOCAL(bridge_action_src_req_, \
2105 morph::model::detail::actionSourceRegistrationRequired<M, A>())
2134#define BRIDGE_REGISTER_ACTION_SOURCE(M, A) \
2135 MORPH_DETAIL_DEFINE_SOURCE_LOCAL(morph::model::detail::actionSourceRegistrationRequired<M, A>()) \
2136 BRIDGE_DETAIL_ACTION_REGISTRARS(M, A)
2148#define BRIDGE_REGISTER_VALIDATOR(A, FN) \
2150 struct morph::model::ActionValidator<A> { \
2151 static bool ready(const A& action) { return (FN)(action); } \
const std::string & result() const noexcept
Returns the committed action's JSON-encoded result.
const std::string & cause() const noexcept
Returns the underlying failure's message, without this type's prefix.
ActionRecordingError(std::string result, std::string cause)
Constructs the error with a message of the form "action executed but was not recorded: <cause>".
std::function< async::Completion< std::string >(std::string const &typeId, std::string const &actionId, std::string const &bodyJson)> Evaluator
Evaluates an action body without running it: the model's type id, the action id and the JSON body in,...
Structural fingerprint of an action payload's JSON shape.
Compile-time profiler instrumentation: Tracy zones, plots, messages and thread names,...
#define MORPH_ZONE_TEXT(text)
Attaches text to the enclosing scope's MORPH_ZONE; an empty text attaches nothing.
#define MORPH_ZONE(name)
Opens a profiler zone named name that ends with the enclosing scope. At most one per scope.
Exact rational arithmetic for morph model/action values.
Where an action's result holds which reflected types.
Per-action policy deciding how repeated executions are checkpointed into a durable action log.
static constexpr bool coalesce
If true, a checkpoint keeps only the most recent entry per (modelType, entityKey, actionType)....
Traits specialisation that maps an action type to its id, JSON codec, and result type.
Per-action readiness predicate deciding whether an action may execute.
static constexpr bool ready(const Action &action)
Returns true if the action is in a state that should be executed.
DuplicateRegistrationError(std::string_view what, std::string_view taken, std::type_index held, std::type_index incoming)
Constructs the error naming the id and both types.
Traits specialisation that maps a model type to its string type-id.
ValidationError(std::string_view modelType, std::string_view actionType)
Constructs the error with a message of the form "action failed validation: <modelType>/<actionType>".
std::function< void()> Action
A widget event handler taking no arguments.