morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
changes.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <glaze/glaze.hpp>
6#include <optional>
7#include <string>
8#include <string_view>
9#include <typeindex>
10#include <utility>
11#include <vector>
12
13#include "change_types.hpp"
14#include "effects.hpp"
15
16/// @file
17/// Deriving an execute's changes from the action's declared effects and its result.
18
19namespace morph::effects {
20
21namespace detail {
22
23/// The JSON tree a result is parsed into. It keeps integers exact, so a 64-bit key is copied as written.
24using ResultJson = glz::generic_u64;
25
26/// @brief Appends the key of one record value to @p out.
27/// @param value A JSON value expected to be one record.
28/// @param keyField The record's key member.
29/// @param out Where to append the key's raw JSON.
30inline void collectKey(ResultJson const& value, std::string_view keyField, std::vector<glz::raw_json>& out) {
31 if (!value.is_object()) {
32 return;
33 }
34 auto const& members = value.get_object();
35 auto const* const key = members.find(keyField);
36 if (key == members.end()) {
37 return;
38 }
39 if (auto text = glz::write_json(key->second)) {
40 out.emplace_back(std::move(*text));
41 }
42}
43
44/// @brief Appends the keys of the records held at a value that the path has reached.
45///
46/// An object is one record when it has the key member, and otherwise a map whose values are records.
47/// @param value The value at the end of a path.
48/// @param keyField The record's key member.
49/// @param out Where to append.
50// NOLINTNEXTLINE(misc-no-recursion): an array nests records to any depth, and the walk follows it.
51inline void collectKeysAtLeaf(ResultJson const& value, std::string_view keyField, std::vector<glz::raw_json>& out) {
52 if (value.is_array()) {
53 for (auto const& element : value.get_array()) {
54 collectKeysAtLeaf(element, keyField, out);
55 }
56 } else if (value.is_object()) {
57 auto const& members = value.get_object();
58 if (members.find(keyField) != members.end()) {
59 collectKey(value, keyField, out);
60 } else {
61 for (auto const& [name, entry] : members) {
62 collectKey(entry, keyField, out);
63 }
64 }
65 }
66}
67
68/// @brief Walks the remaining path segments from @p value, then collects the keys found there.
69///
70/// A vector or optional adds no segment, so an array is entered without consuming one. A map adds
71/// none either, so an object that lacks the next segment is searched through its values.
72/// @param value The current value.
73/// @param path The dot-joined member names; `""` is the value itself.
74/// @param keyField The record's key member.
75/// @param out Where to append.
76// NOLINTNEXTLINE(misc-no-recursion): one call per path segment or nested array, bounded by the result's depth.
77inline void collectKeysAtPath(ResultJson const& value, std::string_view path, std::string_view keyField,
78 std::vector<glz::raw_json>& out) {
79 if (path.empty()) {
80 collectKeysAtLeaf(value, keyField, out);
81 return;
82 }
83 if (value.is_array()) {
84 for (auto const& element : value.get_array()) {
85 collectKeysAtPath(element, path, keyField, out);
86 }
87 return;
88 }
89 if (!value.is_object()) {
90 return;
91 }
92 auto const dot = path.find('.');
93 auto const segment = path.substr(0, dot);
94 auto const rest = dot == std::string_view::npos ? std::string_view{} : path.substr(dot + 1);
95 auto const& members = value.get_object();
96 if (auto const* const next = members.find(segment); next != members.end()) {
97 collectKeysAtPath(next->second, rest, keyField, out);
98 return;
99 }
100 for (auto const& [name, entry] : members) {
101 collectKeysAtPath(entry, path, keyField, out);
102 }
103}
104
105/// @brief Adds a change for a record, merging with one already there.
106///
107/// A whole-type change absorbs keys, and keys are not repeated.
108/// @param changes The changes so far.
109/// @param record The record's wire name.
110/// @param keys The changed keys, or `nullopt` for the whole type.
111inline void addRecordChange(Changes& changes, std::string const& record,
112 std::optional<std::vector<glz::raw_json>> keys) {
113 auto entry = std::ranges::find(changes.records, record, &RecordChange::record);
114 if (entry == changes.records.end()) {
115 changes.records.push_back(RecordChange{.record = record, .keys = std::move(keys)});
116 return;
117 }
118 if (!keys) {
119 entry->keys.reset();
120 return;
121 }
122 if (!entry->keys) {
123 return;
124 }
125 for (auto& key : *keys) {
126 auto const same = [&](glz::raw_json const& have) { return have.str == key.str; };
127 if (std::ranges::none_of(*entry->keys, same)) {
128 entry->keys->push_back(std::move(key));
129 }
130 }
131}
132
133/// @brief Adds a whole-type change for every record derived, directly or through others, from @p source.
134/// @param resolved The resolved registries.
135/// @param source A changed record's wire name.
136/// @param changes Where to add.
137inline void addDerivedChanges(Resolved const& resolved, std::string const& source, Changes& changes) {
138 auto const* const root = resolved.recordNamed(source);
139 if (root == nullptr) {
140 return;
141 }
142 std::vector<std::type_index> reached{root->type};
143 // A record is visited once, which also ends a walk over records that share a source.
144 for (std::size_t next = 0; next < reached.size(); ++next) {
145 // A copy, not a reference: the loop below appends to `reached`.
146 auto const current = reached.at(next);
147 for (auto const& candidate : resolved.records) {
148 if (std::ranges::find(candidate.derivedFrom, current) == candidate.derivedFrom.end() ||
149 std::ranges::find(reached, candidate.type) != reached.end()) {
150 continue;
151 }
152 reached.push_back(candidate.type);
153 addRecordChange(changes, candidate.name, std::nullopt);
154 }
155 }
156}
157
158/// @brief Adds the change an execute's instance makes, unless it is already there.
159/// @param changes Where to add.
160/// @param ranOn The instance the action ran on.
161inline void addInstanceChange(Changes& changes, InstanceChange const& ranOn) {
162 auto const same = [&](InstanceChange const& have) {
163 return have.modelType == ranOn.modelType && have.instance == ranOn.instance && have.primary == ranOn.primary;
164 };
165 if (std::ranges::none_of(changes.instances, same)) {
166 changes.instances.push_back(ranOn);
167 }
168}
169
170/// @brief Adds the change an `updates<R>` effect makes: the keys of the `R` rows found in the result.
171///
172/// A result that does not parse, or a record without a key, cannot name rows: the whole type is reported,
173/// which over-notifies but never misses.
174/// @param resolved The resolved registries.
175/// @param actionId The action's id.
176/// @param record The updated record's wire name.
177/// @param parsed The parsed result, or `nullptr` when it did not parse.
178/// @param changes Where to add.
179inline void addUpdatesChange(Resolved const& resolved, std::string_view actionId, std::string const& record,
180 ResultJson const* parsed, Changes& changes) {
181 auto const* const info = resolved.recordNamed(record);
182 if (parsed == nullptr || info == nullptr || !info->keyField) {
183 addRecordChange(changes, record, std::nullopt);
184 return;
185 }
186 std::vector<glz::raw_json> keys;
187 for (auto const& held : resolved.recordsOf(actionId)) {
188 if (held.record != record) {
189 continue;
190 }
191 for (auto const& path : held.paths) {
192 collectKeysAtPath(*parsed, path, *info->keyField, keys);
193 }
194 }
195 addRecordChange(changes, record, std::move(keys));
196}
197
198/// @brief See `morph::effects::deriveChanges`, over given registries.
199/// @param resolved The resolved registries.
200/// @param actionId The action's id.
201/// @param resultJson The action's result as JSON.
202/// @param ranOn The instance the action ran on.
203/// @return The changes.
204// NOLINTNEXTLINE(bugprone-easily-swappable-parameters): an id, then a JSON document; swapped, no effects are found.
205[[nodiscard]] inline Changes deriveChangesFrom(Resolved const& resolved, std::string_view actionId,
206 std::string_view resultJson, InstanceChange const& ranOn) {
208 auto const declared = resolved.effectsOf(actionId);
209 if (!declared) {
210 return changes;
211 }
212 // The result is parsed once, and only when an `updates` effect needs its rows.
213 std::optional<ResultJson> parsed;
214 bool parseTried = false;
215 auto const parsedResult = [&]() -> ResultJson const* {
216 if (!parseTried) {
217 parseTried = true;
218 ResultJson tree;
219 if (!glz::read_json(tree, resultJson)) {
220 parsed = std::move(tree);
221 }
222 }
223 return parsed ? &*parsed : nullptr;
224 };
225 for (auto const& effect : *declared) {
226 switch (effect.kind) {
227 case Kind::None:
228 break;
231 break;
232 case Kind::Changes:
233 addRecordChange(changes, effect.record, std::nullopt);
234 addDerivedChanges(resolved, effect.record, changes);
235 break;
236 case Kind::Updates:
237 addUpdatesChange(resolved, actionId, effect.record, parsedResult(), changes);
238 addDerivedChanges(resolved, effect.record, changes);
239 break;
240 default:
241 break;
242 }
243 }
244 // A record naming more rows than a reader accepts is reported whole: it over-notifies, never misses.
245 for (auto& change : changes.records) {
246 if (change.keys && change.keys->size() > kMaxChangeKeys) {
247 change.keys.reset();
248 }
249 }
250 return changes;
251}
252
253} // namespace detail
254
255/// @brief The changes a successful execute made, from the action's declared effects and its result.
256///
257/// `updates<R>` gives the keys of the `R` rows found in the result, `changes<R>` the whole type, and both
258/// also every record derived from `R`, directly or through others, as a whole-type change.
259/// `changesInstance` gives @p ranOn. An action that declared nothing, or only `none`, gives no changes.
260/// @param actionId The action's id.
261/// @param resultJson The action's result as JSON.
262/// @param ranOn The instance the action ran on.
263/// @return The changes; empty for a reader.
264/// @throws morph::model::EffectsRegistrationError when the registered configuration is invalid.
265[[nodiscard]] inline Changes deriveChanges(std::string_view actionId, std::string_view resultJson,
266 InstanceChange const& ranOn) {
267 return detail::deriveChangesFrom(detail::resolved(), actionId, resultJson, ranOn);
268}
269
270} // namespace morph::effects
constexpr std::size_t kMaxChangeKeys
The most keys one RecordChange carries.
Changes deriveChangesFrom(Resolved const &resolved, std::string_view actionId, std::string_view resultJson, InstanceChange const &ranOn)
See morph::effects::deriveChanges, over given registries.
Definition changes.hpp:205
glz::generic_u64 ResultJson
The JSON tree a result is parsed into. It keeps integers exact, so a 64-bit key is copied as written.
Definition changes.hpp:24
Changes deriveChanges(std::string_view actionId, std::string_view resultJson, InstanceChange const &ranOn)
The changes a successful execute made, from the action's declared effects and its result.
Definition changes.hpp:265
void collectKeysAtLeaf(ResultJson const &value, std::string_view keyField, std::vector< glz::raw_json > &out)
Appends the keys of the records held at a value that the path has reached.
Definition changes.hpp:51
void collectKey(ResultJson const &value, std::string_view keyField, std::vector< glz::raw_json > &out)
Appends the key of one record value to out.
Definition changes.hpp:30
void addDerivedChanges(Resolved const &resolved, std::string const &source, Changes &changes)
Adds a whole-type change for every record derived, directly or through others, from source.
Definition changes.hpp:137
void addUpdatesChange(Resolved const &resolved, std::string_view actionId, std::string const &record, ResultJson const *parsed, Changes &changes)
Adds the change an updates<R> effect makes: the keys of the R rows found in the result.
Definition changes.hpp:179
void addInstanceChange(Changes &changes, InstanceChange const &ranOn)
Adds the change an execute's instance makes, unless it is already there.
Definition changes.hpp:161
void collectKeysAtPath(ResultJson const &value, std::string_view path, std::string_view keyField, std::vector< glz::raw_json > &out)
Walks the remaining path segments from value, then collects the keys found there.
Definition changes.hpp:77
void addRecordChange(Changes &changes, std::string const &record, std::optional< std::vector< glz::raw_json > > keys)
Adds a change for a record, merging with one already there.
Definition changes.hpp:111
Resolved const & resolved()
The process registries, matched and validated on the first call.
Definition effects.hpp:511
@ Updates
updates<R>
Definition effects.hpp:91
@ Changes
changes<R>
Definition effects.hpp:92
@ ChangesInstance
changesInstance
Definition effects.hpp:93
What one successful execute changed.
A change to the state of one model instance.
std::uint64_t instance
The instance id.
std::string primary
The directory key's text for a keyed shared instance; empty for a private one.
std::string modelType
The model's registered type name.
A change to the rows of one record type.
std::string record
The record's wire name.
Effect: the action may create, remove or change rows of record R, which its result need not carry.
Definition effects.hpp:81
The matched and validated registries: what every read is answered from.
Definition effects.hpp:232