28#include <glaze/glaze.hpp>
36#include <unordered_map>
39#include "../attributes.hpp"
40#include "../core/bridge.hpp"
41#include "../core/callback_scope.hpp"
42#include "../core/logger.hpp"
46namespace morph::flows {
55template <morph::forms::FixedString Field, morph::forms::FixedString Path>
69 using binds = std::tuple<Binds...>;
73 [[nodiscard]]
static constexpr std::string_view
title() noexcept {
return Title.view(); }
82 using steps = std::tuple<Steps...>;
86 [[nodiscard]]
static constexpr std::string_view
title() noexcept {
return Title.view(); }
110 glz::generic_u64 dom{};
111 dom[
"w-title"] = std::string{W::title()};
113 glz::generic_u64::array_t steps{};
114 ::morph::forms::detail::forEachTupleElement<typename W::steps>([&]<
typename StepT, std::size_t I>() {
115 static_cast<void>(I);
116 glz::generic_u64 step{};
118 step[
"title"] = std::string{StepT::title()};
119 if constexpr (std::tuple_size_v<typename StepT::binds> != 0) {
124 ::morph::forms::detail::emitBindsInto<typename StepT::binds>(step[
"prefill"]);
126 steps.emplace_back(std::move(step));
128 dom[
"w-steps"] = steps;
130 return glz::write_json(dom).value_or(std::string{});
152template <
typename Model,
typename... Steps>
154 static_assert(
sizeof...(Steps) > 0,
"FlowSession: a flow needs at least one step");
155 static_assert(::morph::forms::detail::AllDistinct<Steps...>::value,
156 "FlowSession: step action types must be pairwise distinct");
177 std::function<
void(std::exception_ptr)> onError MORPH_LIFETIMEBOUND =
nullptr)
178 : _handler{handler}, _onError{std::move(onError)} {
205 note(
"FlowSession::~FlowSession");
206 _callbacks.requestStop();
236 template <auto FieldPtr>
237 void set(typename ::morph::bridge::detail::MemberPointerTraits<
decltype(FieldPtr)>::ValueType value) {
238 using A = typename ::morph::bridge::detail::MemberPointerTraits<
decltype(FieldPtr)>::ClassType;
239 static_assert((std::is_same_v<A, Steps> || ...),
240 "FlowSession::set<>: field's action is not a step of this flow");
241 note(
"FlowSession::set");
243 throw std::logic_error{
"FlowSession::set<>: field belongs to an action that is not the current step"};
245 std::get<A>(_drafts).*FieldPtr = std::move(value);
246 A draft = std::get<A>(_drafts);
247 std::size_t
const stepIndex = _activeStep;
254 fireStep<A>(std::move(draft), stepIndex);
264 note(
"FlowSession::advance");
269 _currentReady =
false;
274 _activeStep = _index;
286 note(
"FlowSession::back");
291 _currentReady =
true;
292 _activeStep = _index;
299 [[nodiscard]]
bool finished() const noexcept {
return _index >=
sizeof...(Steps); }
303 [[nodiscard]]
bool ready() const noexcept {
304 note(
"FlowSession::ready");
305 return _currentReady;
310 [[nodiscard]] std::size_t
currentIndex() const noexcept {
return _index; }
314 [[nodiscard]]
static constexpr std::size_t
stepCount() noexcept {
return sizeof...(Steps); }
319 std::string_view
id{};
320 ::morph::forms::detail::forPackElement<Steps...>(
331 [[nodiscard]] std::optional<std::string>
resolved(std::string_view path)
const {
332 note(
"FlowSession::resolved");
333 auto iter = _resolvedValues.find(std::string{path});
334 if (iter == _resolvedValues.end()) {
341 template <
typename A>
342 void captureResult(const ::morph::model::ActionTraits<A>::Result& result, std::size_t stepIndex) {
343 note(
"FlowSession::captureResult");
344 if (stepIndex != _activeStep) {
357 auto record = [&](
const auto& value) {
358 ::morph::forms::detail::forEachNamedMember(
359 value, [&]<std::size_t I>(std::string_view name,
const auto& member) {
360 static_cast<void>(I);
368 if (!glz::write_json(member, json)) {
369 _resolvedValues[std::string{typeId} +
"." + std::string{name}] = std::move(json);
373 record(std::get<A>(_drafts));
375 _currentReady =
true;
387 template <
typename A>
388 void fireStep(A draft, std::size_t stepIndex) {
389 _handler.execute(std::move(draft))
391 [
this, stepIndex](::morph::model::ActionTraits<A>::Result result) {
392 this->
template captureResult<A>(result, stepIndex);
394 .onError(_callbacks, [
this, stepIndex](
const std::exception_ptr& err) {
395 note(
"FlowSession::fireStep");
401 if (stepIndex == _activeStep) {
402 _currentReady =
false;
407 logUnhandledError(::morph::model::ActionTraits<A>::typeId(), err);
412 static void logUnhandledError(std::string_view typeId,
const std::exception_ptr& err) {
414 std::rethrow_exception(err);
415 }
catch (
const std::exception& exc) {
416 ::morph::log::logError(std::string{
"[flow:"} + std::string{typeId} +
417 "] unhandled exception: " + exc.what());
419 ::morph::log::logError(std::string{
"[flow:"} + std::string{typeId} +
"] unhandled unknown exception");
427 void beginStep() { _activeStep = _index; }
431 void note(
char const* site)
const noexcept {
432 ::morph::exec::detail::noteOwner(site, _handler.owner().coreExecutor(), _handler.onOwner());
435 ::morph::bridge::BridgeHandler<Model>& _handler;
436 std::function<void(std::exception_ptr)> _onError;
440 std::size_t _index{0};
441 std::tuple<Steps...> _drafts{};
444 std::size_t _activeStep{0};
445 bool _currentReady{
false};
446 std::unordered_map<std::string, std::string> _resolvedValues;
450 ::morph::async::CallbackScope _callbacks;
464#define BRIDGE_REGISTER_WIZARD(W, NAME) \
466 struct morph::flows::WizardTraits<W> { \
467 static constexpr std::string_view typeId() noexcept { return NAME; } \
::morph::detail::FixedString< N > FixedString
A structural, NTTP-capable compile-time string (for naming the options action and its value/label res...
RAII wrapper that binds a single model type to a Bridge.
Sequences an ordered list of registered actions (Steps...) as one multi-step flow sharing captured va...
~FlowSession()
Stops every callback this session installed from being delivered.
bool back()
Returns to the previous step. Its draft — this session's own std::get<A>(_drafts) slot,...
FlowSession(::morph::bridge::BridgeHandler< Model > &handler, std::function< void(std::exception_ptr)> onError=nullptr)
Constructs a flow over handler, starting at step 0.
static constexpr std::size_t stepCount() noexcept
The number of steps in this flow.
std::optional< std::string > resolved(std::string_view path) const
Looks up a value captured from an earlier step's submitted draft or result.
std::string_view currentActionType() const noexcept
The registered type-id of the current step's action.
bool ready() const noexcept
Whether the current step already has a captured, successful result.
std::size_t currentIndex() const noexcept
The current 0-based step position.
void set(typename ::morph::bridge::detail::MemberPointerTraits< decltype(FieldPtr)>::ValueType value)
Sets one field of the current step's draft and dispatches the step as soon as its ActionValidator rep...
bool finished() const noexcept
Whether the flow has advanced past the last step.
bool advance()
Moves to the next step, if the current step has already produced a successful result (a not-ready ste...
morph::forms::Bind< Field, Path > Bind
Prefill binding for a wizard step.
std::string wizardSchemaJson()
Generates the w-* JSON document for wizard type W.
Machinery shared by the two form session types: morph::flows::FlowSession (ordered wizard steps) and ...
One step of a Wizard: a registered action, a display title, and zero or more Bind prefill declaration...
std::tuple< Binds... > binds
Tuple of this step's Bind<...> prefill declarations (possibly empty).
Action action
The step's action type.
static constexpr std::string_view title() noexcept
The step's display title.
Traits specialisation mapping a Wizard type to its string type-id.
An ordered sequence of WizardSteps sharing one flow.
std::tuple< Steps... > steps
Tuple of this wizard's ordered WizardStep<...> types.
static constexpr std::string_view title() noexcept
The wizard's display title.
Traits specialisation that maps an action type to its id, JSON codec, and result type.
static constexpr bool ready(const Action &action)
Returns true if the action is in a state that should be executed.