morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
sections.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file forms/sections.hpp
6/// @brief Unordered sections: N independently editable action drafts on one
7/// screen, each with its own readiness gate and no sequencing.
8///
9/// The sibling to `morph::flows::FlowSession`. A wizard has one active step and
10/// throws on a field belonging to any other; a section set has no active
11/// section at all -- every declared section is editable at every moment, and
12/// each dispatches on its own as soon as its draft validates.
13///
14/// Choose this when order carries no meaning (tabs, cards, a settings page) and
15/// `FlowSession` when it does. Like flows, this is additive metadata and
16/// dispatch bookkeeping over the ordinary `BridgeHandler<Model>::execute<A>()`
17/// path: no new wire format, no new execution mode.
18/// See docs/spec/forms/sections.md.
19
20#include <cstddef>
21#include <exception>
22#include <functional>
23#include <glaze/glaze.hpp>
24#include <optional>
25#include <string>
26#include <string_view>
27#include <tuple>
28#include <type_traits>
29#include <unordered_map>
30#include <utility>
31
32#include "../attributes.hpp"
33#include "../core/bridge.hpp"
34#include "../core/callback_scope.hpp"
35#include "../core/logger.hpp"
37
38namespace morph::forms {
39
40/// @brief One section of a `SectionGroup`: a registered action, a display
41/// title, and zero or more `Bind` prefill declarations.
42/// @tparam Action Registered action type (`BRIDGE_REGISTER_ACTION`) this section fires.
43/// @tparam Title Human title for the section (tab label / card header).
44/// @tparam Binds Zero or more `Bind<Field, Path>` prefill declarations.
45template <typename Action, morph::forms::FixedString Title, typename... Binds>
46struct Section {
47 /// @brief The section's action type.
48 using action = Action;
49
50 /// @brief Tuple of this section's `Bind<...>` prefill declarations (possibly empty).
51 using binds = std::tuple<Binds...>;
52
53 /// @brief The section's display title.
54 /// @return The declared title.
55 [[nodiscard]] static constexpr std::string_view title() noexcept { return Title.view(); }
56};
57
58/// @brief An unordered set of `Section`s sharing one screen.
59/// @tparam Title Human title for the whole group.
60/// @tparam Sections One or more `Section<Action, Title, Binds...>` types.
61template <morph::forms::FixedString Title, typename... Sections>
63 /// @brief Tuple of this group's `Section<...>` types.
64 using sections = std::tuple<Sections...>;
65
66 /// @brief The group's display title.
67 /// @return The declared title.
68 [[nodiscard]] static constexpr std::string_view title() noexcept { return Title.view(); }
69};
70
71/// @brief Traits specialisation mapping a `SectionGroup` type to its string type-id.
72///
73/// Specialise via `BRIDGE_REGISTER_SECTION_GROUP` rather than by hand. The
74/// default is a forward declaration — using it without a specialisation is an
75/// incomplete-type error. The schema needs a stable name for the group;
76/// deriving one from the C++ type would tie the wire format to a mangled name.
77/// @tparam G Concrete `SectionGroup<...>` type.
78template <typename G>
79struct SectionGroupTraits; // forward — specialise or use BRIDGE_REGISTER_SECTION_GROUP
80
81/// @brief Generates the `s-*` JSON document for section group @p G.
82///
83/// Emits `s-id`, `s-title`, and an `s-sections` array; each entry carries
84/// `action` (the section's registered action type-id), `title`, and — only when
85/// the section declares at least one `Bind` — a `prefill` object mapping field
86/// name to `"<ActionTypeId>.<field>"` path.
87///
88/// Deliberately emits no index or order field. A renderer arranges the sections
89/// itself, and a position in the document would suggest a sequence this type
90/// does not have.
91/// @tparam G Concrete `SectionGroup<Title, Sections...>` type.
92/// @return The group's JSON document. Empty string only if glaze's own JSON
93/// writer fails on the assembled DOM (schema generation never throws).
94// NOLINTBEGIN(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access) -- glaze DOM requires operator[]
95template <typename G>
96[[nodiscard]] std::string sectionGroupSchemaJson() {
97 glz::generic_u64 dom{};
98 dom["s-id"] = std::string{SectionGroupTraits<G>::typeId()};
99 dom["s-title"] = std::string{G::title()};
100
101 glz::generic_u64::array_t sections{};
102 detail::forEachTupleElement<typename G::sections>([&]<typename SectionT, std::size_t I>() {
103 static_cast<void>(I);
104 glz::generic_u64 entry{};
106 entry["title"] = std::string{SectionT::title()};
107 if constexpr (std::tuple_size_v<typename SectionT::binds> != 0) {
108 detail::emitBindsInto<typename SectionT::binds>(entry["prefill"]);
109 }
110 sections.emplace_back(std::move(entry));
111 });
112 dom["s-sections"] = sections;
113
114 return glz::write_json(dom).value_or(std::string{});
115}
116// NOLINTEND(cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
117
118/// @brief Drives N independently editable action drafts on one screen.
119///
120/// Each section accumulates its own draft through `set<>` and dispatches
121/// through the handler as soon as `ActionValidator<A>::ready` accepts it.
122/// There is no active section and no sequence: a field belonging to any
123/// declared section may be set at any time, in any order. This is the whole
124/// difference from `morph::flows::FlowSession`, which throws on a field
125/// outside its current step.
126///
127/// A ready section re-fires on every subsequent `set<>`. There is no latch and
128/// no coalescing, matching `FlowSession`; a caller that wants one request per
129/// pause debounces on its own side, where it knows what a pause means.
130/// @tparam Model The model the handler is bound to.
131/// @tparam Sections One or more `Section<Action, Title, Binds...>` types.
132template <typename Model, typename... Sections>
134 static_assert(sizeof...(Sections) > 0, "SectionSet: a group needs at least one section");
135 static_assert(::morph::forms::detail::AllDistinct<typename Sections::action...>::value,
136 "SectionSet: section action types must be pairwise distinct");
137
138public:
139 /// @brief Constructs a section set dispatching through @p handler.
140 /// @param handler Handler every section dispatches through. Must outlive
141 /// this `SectionSet`, which belongs to the handler's owner:
142 /// it is built, used and destroyed there, and the handler's
143 /// `guiExec` delivers its continuations there.
144 /// @param onError Optional callback invoked when a section's dispatch
145 /// fails. When absent, the error is logged via
146 /// `morph::log::logError`. Stored and invoked for this
147 /// object's whole lifetime, so anything the callable refers
148 /// to must outlive it.
149 explicit SectionSet(::morph::bridge::BridgeHandler<Model>& handler MORPH_LIFETIMEBOUND,
150 std::function<void(std::exception_ptr)> onError MORPH_LIFETIMEBOUND = nullptr)
151 : _handler{handler}, _onError{std::move(onError)} {}
152
153 SectionSet(const SectionSet&) = delete;
154 SectionSet& operator=(const SectionSet&) = delete;
155 SectionSet(SectionSet&&) = delete;
156 SectionSet& operator=(SectionSet&&) = delete;
157
158 /// @brief Stops every callback this set installed from being delivered.
159 ///
160 /// There is nothing to detach: a section's continuations are owned by the
161 /// in-flight dispatch, not held in a map this object could remove itself
162 /// from. `_callbacks` gates each one on a token it checks before touching
163 /// `this`, so a completion resolving after this object is gone finds the
164 /// token stopped and returns without dereferencing anything.
165 ///
166 /// `requestStop()` is called explicitly rather than left to the member's
167 /// own destruction, even though `_callbacks` is declared last: members are
168 /// destroyed only *after* the destructor body runs, so a body that later
169 /// grew a call pumping an event loop would otherwise deliver into a
170 /// half-dead object. The body does not do that today; stopping first keeps
171 /// it correct if one is ever added.
172 ///
173 /// Destroyed on the handler's owner, where its continuations run, so the
174 /// check a continuation makes and this destructor cannot interleave.
176 note("SectionSet::~SectionSet");
177 _callbacks.requestStop();
178 }
179
180 /// @brief Sets one field of its section's draft, dispatching that section
181 /// if the draft is now ready.
182 ///
183 /// Unlike `FlowSession::set<>` this imposes no ordering: the field's action
184 /// need only be one of the declared sections. On the handler's owner.
185 /// @tparam FieldPtr Pointer-to-data-member of a declared section's action struct.
186 /// @param value New value for the field.
187 template <auto FieldPtr>
188 void set(::morph::bridge::detail::MemberPointerTraits<decltype(FieldPtr)>::ValueType value) {
189 using A = ::morph::bridge::detail::MemberPointerTraits<decltype(FieldPtr)>::ClassType;
190 static_assert((std::is_same_v<A, typename Sections::action> || ...),
191 "SectionSet::set<>: the field's action is not a section of this group");
192 note("SectionSet::set");
193 std::get<A>(_drafts).*FieldPtr = std::move(value);
194 if (::morph::model::ActionValidator<A>::ready(std::get<A>(_drafts))) {
195 fire<A>(std::get<A>(_drafts));
196 }
197 }
198
199 /// @brief Clears one section's draft back to a default-constructed action.
200 ///
201 /// Touches no other section and dispatches nothing. Values already captured
202 /// from a previous successful dispatch stay in `resolved()`: they describe
203 /// what the model was told, which resetting an editor does not undo.
204 /// @tparam A The section's action type.
205 template <typename A>
206 void reset() {
207 static_assert((std::is_same_v<A, typename Sections::action> || ...),
208 "SectionSet::reset<>: not a section of this group");
209 note("SectionSet::reset");
210 std::get<A>(_drafts) = A{};
211 }
212
213 /// @brief Snapshots one section's current draft.
214 ///
215 /// A copy, not a reference into live state. On the handler's owner.
216 /// @tparam A The section's action type.
217 /// @return The draft as it stands.
218 template <typename A>
219 [[nodiscard]] A draft() const {
220 static_assert((std::is_same_v<A, typename Sections::action> || ...),
221 "SectionSet::draft<>: not a section of this group");
222 note("SectionSet::draft");
223 return std::get<A>(_drafts);
224 }
225
226 /// @brief Looks up a value captured from a section's submitted draft or result.
227 /// @param path `"<ActionTypeId>.<field>"`, matching a section's declared `Bind::path()`.
228 /// @return The field's JSON-encoded value, or `std::nullopt` if @p path was
229 /// never captured — the section never fired successfully, or has no
230 /// such field.
231 [[nodiscard]] std::optional<std::string> resolved(std::string_view path) const {
232 note("SectionSet::resolved");
233 auto iter = _resolvedValues.find(std::string{path});
234 if (iter == _resolvedValues.end()) {
235 return std::nullopt;
236 }
237 return iter->second;
238 }
239
240private:
241 /// @brief Records section @p A's submitted draft and its result under
242 /// `"<ActionTypeId>.<field>"` keys.
243 ///
244 /// Draft fields go in first and result fields overwrite them on a name
245 /// collision: the result is what the model actually settled on, so it is
246 /// the value a later section should prefill from.
247 /// @tparam A Section action type.
248 /// @param result The dispatch's successful result.
249 template <typename A>
250 void captureResult(const ::morph::model::ActionTraits<A>::Result& result) {
251 note("SectionSet::captureResult");
252 auto const typeId = ::morph::model::ActionTraits<A>::typeId();
253 auto record = [&](const auto& value) {
254 ::morph::forms::detail::forEachNamedMember(
255 value, [&]<std::size_t I>(std::string_view name, const auto& member) {
256 static_cast<void>(I);
257 std::string json;
258 // The `!write_json(...)` guard's false arm (write failure)
259 // is not exercised by this file's own test suite: every
260 // section action's fields are plain, well-formed data
261 // glaze's JSON writer cannot fail on. The same
262 // "untestable line" flows.hpp's captureResult already
263 // carries, for the same reason.
264 if (!glz::write_json(member, json)) {
265 _resolvedValues[std::string{typeId} + "." + std::string{name}] = std::move(json);
266 }
267 });
268 };
269 record(std::get<A>(_drafts));
270 record(result);
271 }
272
273 /// @brief Reports a dispatch failure no `onError` callback was given for.
274 /// @param typeId The failing section's action type-id.
275 /// @param err The captured exception.
276 static void logUnhandledError(std::string_view typeId, const std::exception_ptr& err) {
277 try {
278 std::rethrow_exception(err);
279 } catch (const std::exception& exc) {
280 ::morph::log::logError(std::string{"[section:"} + std::string{typeId} +
281 "] unhandled exception: " + exc.what());
282 } catch (...) {
283 ::morph::log::logError(std::string{"[section:"} + std::string{typeId} + "] unhandled unknown exception");
284 }
285 }
286
287 /// @brief Dispatches section @p A's ready draft and routes its outcome.
288 ///
289 /// Both closures run on the handler's `guiExec`, which must be its owner,
290 /// and are gated on `_callbacks`, so neither touches anything on `this`
291 /// once the set has been destroyed — a completion can still resolve after
292 /// the set is gone.
293 ///
294 /// Nothing here is keyed to a "current" section, which is what makes
295 /// sections cheaper than flow steps: a late reply cannot be stale, because
296 /// there is no position for it to be stale relative to.
297 /// @tparam A Section action type.
298 /// @param draft The ready action to execute.
299 template <typename A>
300 void fire(A draft) {
301 _handler.execute(std::move(draft))
302 .then(_callbacks,
303 [this](const ::morph::model::ActionTraits<A>::Result& result) {
304 this->template captureResult<A>(result);
305 })
306 .onError(_callbacks, [this](const std::exception_ptr& err) {
307 if (_onError) {
308 _onError(err);
309 } else {
310 logUnhandledError(::morph::model::ActionTraits<A>::typeId(), err);
311 }
312 });
313 }
314
315 /// @brief Checks, in a debug build, that @p site runs on the handler's owner.
316 /// @param site Name of the calling body.
317 void note(char const* site) const noexcept {
318 ::morph::exec::detail::noteOwner(site, _handler.owner().coreExecutor(), _handler.onOwner());
319 }
320
321 ::morph::bridge::BridgeHandler<Model>& _handler;
322 std::function<void(std::exception_ptr)> _onError;
323 // Touched only on the handler's owner: by set/reset/draft/resolved, and by
324 // a dispatch's continuation, delivered on the handler's `guiExec` — the
325 // owner.
326 std::tuple<typename Sections::action...> _drafts{};
327 std::unordered_map<std::string, std::string> _resolvedValues;
328 // Declared last, so it is the first member destroyed: every gated callback
329 // is stopped before the state those callbacks touch goes away.
330 ::morph::async::CallbackScope _callbacks;
331};
332
333} // namespace morph::forms
334
335// clang-format off -- public macro surface; see CONTRIBUTING.md, "Formatting/linting".
336// NOLINTBEGIN(cppcoreguidelines-macro-usage) — registration macro is the intended public API
337/// @brief Specialises `morph::forms::SectionGroupTraits<G>` with the string type-id @p NAME.
338#define BRIDGE_REGISTER_SECTION_GROUP(G, NAME) \
339 template <> \
340 struct morph::forms::SectionGroupTraits<G> { \
341 static constexpr std::string_view typeId() noexcept { return NAME; } \
342 };
343// clang-format on
344// NOLINTEND(cppcoreguidelines-macro-usage)
::morph::detail::FixedString< N > FixedString
A structural, NTTP-capable compile-time string (for naming the options action and its value/label res...
Definition choice.hpp:65
RAII wrapper that binds a single model type to a Bridge.
Definition bridge.hpp:2583
Drives N independently editable action drafts on one screen.
Definition sections.hpp:133
void reset()
Clears one section's draft back to a default-constructed action.
Definition sections.hpp:206
A draft() const
Snapshots one section's current draft.
Definition sections.hpp:219
std::optional< std::string > resolved(std::string_view path) const
Looks up a value captured from a section's submitted draft or result.
Definition sections.hpp:231
SectionSet(::morph::bridge::BridgeHandler< Model > &handler, std::function< void(std::exception_ptr)> onError=nullptr)
Constructs a section set dispatching through handler.
Definition sections.hpp:149
void set(::morph::bridge::detail::MemberPointerTraits< decltype(FieldPtr)>::ValueType value)
Sets one field of its section's draft, dispatching that section if the draft is now ready.
Definition sections.hpp:188
~SectionSet()
Stops every callback this set installed from being delivered.
Definition sections.hpp:175
std::string sectionGroupSchemaJson()
Generates the s-* JSON document for section group G.
Definition sections.hpp:96
Machinery shared by the two form session types: morph::flows::FlowSession (ordered wizard steps) and ...
Traits specialisation mapping a SectionGroup type to its string type-id.
Definition sections.hpp:79
An unordered set of Sections sharing one screen.
Definition sections.hpp:62
static constexpr std::string_view title() noexcept
The group's display title.
Definition sections.hpp:68
std::tuple< Sections... > sections
Tuple of this group's Section<...> types.
Definition sections.hpp:64
One section of a SectionGroup: a registered action, a display title, and zero or more Bind prefill de...
Definition sections.hpp:46
std::tuple< Binds... > binds
Tuple of this section's Bind<...> prefill declarations (possibly empty).
Definition sections.hpp:51
Action action
The section's action type.
Definition sections.hpp:48
static constexpr std::string_view title() noexcept
The section's display title.
Definition sections.hpp:55
Traits specialisation that maps an action type to its id, JSON codec, and result type.
Definition registry.hpp:48
static constexpr bool ready(const Action &action)
Returns true if the action is in a state that should be executed.
Definition registry.hpp:269
std::function< void()> Action
A widget event handler taking no arguments.
Definition view.hpp:33