Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -601,14 +601,19 @@ if(MORPH_BUILD_FORMS_QML)
# not only under morph::qt: forms_controller_core.hpp includes both, and
# morph::qt (MORPH_BUILD_QT, which needs Qt WebSockets) is not part of
# every install that ships this header. Neither needs anything beyond
# QtCore.
# QtCore. multi_model_bridge_core.hpp/multi_model_forms_controller_core.hpp
# and the owned_local_bridge.hpp detail header they (and the single-model
# pair) share are the same story.
target_sources(morph_qt_forms
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/qt/bridge/detail/owned_local_bridge.hpp
include/morph/qt/bridge/generic_model_bridge_core.hpp
include/morph/qt/bridge/multi_model_bridge_core.hpp
include/morph/qt/forms/forms_controller_core.hpp
include/morph/qt/forms/multi_model_forms_controller_core.hpp
include/morph/qt/qt_executor.hpp
)
endif()
Expand Down
1 change: 1 addition & 0 deletions docs/spec/core/bridge.md
Original file line number Diff line number Diff line change
Expand Up @@ -1100,6 +1100,7 @@ make teardown order-independent.)
| `instance` | `static ActionExecuteRegistry& instance()` | Process-level singleton. |
| `registerAction` | `template<Model, Action> void registerAction(string_view modelId, string_view actionId)` | Registers an executor that deserializes JSON → `ActionTraits::fromJson`, calls `BridgeHandler<Model, Sharing>::execute<>`, serializes result back. Files **two** entries — one per sharing tag (`NoSharing`, `AllowShared`) — from one generic-lambda template. Defined out-of-line after `BridgeHandler`. |
| `execute` | `template<Sharing> Completion<string> execute(string_view modelId, string_view actionId, void* handler, string_view bodyJson) const` | Lookup + invoke, under the caller's own sharing policy. Key is `(modelId, actionId, typeid(Sharing))`. Throws `runtime_error` on an unknown key. |
| `contains` | `template<Sharing> bool contains(string_view modelId, string_view actionId) const noexcept` | Existence check over the same key `execute` looks up, without invoking anything. Since `registerAction` always files both sharing tags together, this answers the same for either `Sharing` for any action registered via `BRIDGE_REGISTER_ACTION`. Backs `BridgeHandler::servesAction`. |

### `Bridge`

Expand Down
29 changes: 26 additions & 3 deletions docs/spec/forms/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -1422,6 +1422,29 @@ renderer for it, Qt/QML, as a reusable component rather than example code.
home: its own `LabFormsDemo` QML module carries only `Main.qml` and the
`FormsController` subclass naming `lab::LabModel`; `Main.qml` imports
`MorphForms` for `DynamicForm`/`I18nCatalog` like any other consumer would.
- **`include/morph/qt/forms/multi_model_forms_controller_core.hpp`** (same
component, same install story) ships
`morph::qt::forms::MultiModelFormsControllerCore<Sharing, Model...>`, the
multi-model sibling of `FormsControllerCore<Model, Sharing>` for a rung
whose forms span more than one registered model
(`bookmarks::gui::FormsBridge`, whose forms serve `AuthModel`,
`BookmarkModel` and `TagModel`, is the shipped example). Same
`schemasJson()`/`submitIfValid()`/`fetchOptions()` surface, over
`morph::qt::bridge::MultiModelBridgeCore<Sharing, Model...>`
(`include/morph/qt/bridge/multi_model_bridge_core.hpp`) instead of
`GenericModelBridgeCore<Model, Sharing>`: it composes one
`GenericModelBridgeCore<Model, Sharing>` per `Model` in the pack and routes a
submitted action-type id to whichever one serves it, via
`GenericModelBridgeCore::servesAction` (which forwards to
`BridgeHandler::servesAction`) — a pure existence check over
`ActionExecuteRegistry`, the same registry `executeJson` dispatches
through — rather than a hand-written `actionType -> Model` table. Models
are tried in the order the pack declares them; an action id registered on
more than one `Model` in the pack is a configuration bug asserted in debug
builds, not a case routed silently. An unrouted action type resolves
`onError` directly with `"no model in this client serves action '<id>'"`,
the same wording every hand-written router before it agreed on
independently.

This is packaging and factoring only: no `x-*` key changed, and a plain
single-action form renders identically to before the renderer was extracted.
Expand All @@ -1435,9 +1458,9 @@ not one, because only one of the two signals is universal:
- **`optionsReceived(optionsAction, ok, payload)` is optional.** It exists only
on a controller that serves a `Choice` field; a controller that serves none
deliberately declares neither it nor `fetchOptions()`
(`bookmarks::gui::BookmarkFormsController` and
`pastebin::gui::FormsBridge` each carry the reasoning: an unused
`fetchOptions()` would be a stub with nothing to call it). Its block gates its
(`bookmarks::gui::FormsBridge` and `pastebin::gui::FormsBridge` each carry
the reasoning: an unused `fetchOptions()` would be a stub with nothing to
call it). Its block gates its
**target** on the signal being declared — `form.controller.optionsReceived
!== undefined`, else `null` — so a controller that omits it is never connected
to and the absence is not a warning. Without the split, every form instance
Expand Down
16 changes: 9 additions & 7 deletions examples/bookmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,15 +452,17 @@ submit button either, and every form is bound to the live controller. The one
non-form input on the whole screen is the per-row selection checkbox, which
types nothing.

Two pieces of glue carry their own written justification, per rule 2's "(b)
`gui_lib/bookmark_qml_bridges.hpp`'s `FormsBridge` composes
`morph::qt::forms::MultiModelFormsControllerCore<NoSharing, AuthModel,
BookmarkModel, TagModel>` directly, over the `Bridge&`/`IExecutor*`
`AppContext::onReady()` hands it — no rung-owned routing controller sits
between them; the shipped core routes each action-type string to whichever of
the three form-serving models owns it, via a plain existence check over
`ActionExecuteRegistry` rather than a hand-written table.

One piece of glue carries its own written justification, per rule 2's "(b)
pure glue with no domain logic" clause:

- `gui::BookmarkFormsController` — composed over an injected
`Bridge&`/`IExecutor*`, like `morph::qt::forms::FormsControllerCore`'s own
composing constructor, plus the one genuinely new part this rung's own
controller owns — routing an action-type string to whichever of the three
form-serving models owns it, which the shipped core (templated over a
single model) has no equivalent for.
- `gui::FormsBridge::onLoginSucceeded` — installs the token the server
returned as the shared `Bridge`'s default session, so every subsequent
action carries it. Infrastructure wiring, not business logic: it decides
Expand Down
37 changes: 0 additions & 37 deletions examples/bookmarks/gui_lib/bookmark_forms_controller.cpp

This file was deleted.

143 changes: 0 additions & 143 deletions examples/bookmarks/gui_lib/bookmark_forms_controller.hpp

This file was deleted.

17 changes: 9 additions & 8 deletions examples/bookmarks/gui_lib/bookmark_qml_bridges.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -130,9 +130,9 @@ std::optional<LoginResult> decodeLoginResult(const std::string& resultJson) {
// ── FormsBridge ─────────────────────────────────────────────────────────────

FormsBridge::FormsBridge(::morph::bridge::Bridge& bridge, ::morph::exec::IExecutor* executor, QObject* parent)
: QObject{parent}, _bridge{bridge}, _controller{bridge, executor, bookmarkSchemasJson()} {}
: QObject{parent}, _bridge{bridge}, _core{bridge, executor, bookmarkSchemasJson()} {}

QString FormsBridge::schemasJson() const { return QString::fromStdString(_controller.schemasJson()); }
QString FormsBridge::schemasJson() const { return QString::fromStdString(_core.schemasJson()); }

void FormsBridge::onLoginSucceeded(const LoginResult& result) {
::morph::session::Context session;
Expand All @@ -148,12 +148,13 @@ void FormsBridge::submitIfValid(const QString& actionType, const QString& bodyJs
// doc comment for the full argument. `_callbacks.guard(...)` is the
// general-purpose gate (`CallbackScope`'s `guard()`, not `Completion`'s
// `then(scope, fn)` overload) because the `Completion` these end up on is
// created and attached *inside* `BookmarkFormsController::submitIfValid`,
// one frame further in; what this function hands over is a pair of plain
// callables. Wrapping them here keeps the controller a
// callback-shape-agnostic seam and puts the gate in the class that owns the
// captured `this`, which is where it belongs.
_controller.submitIfValid(
// created and attached *inside*
// `MultiModelFormsControllerCore::submitIfValid`, one frame further in;
// what this function hands over is a pair of plain callables. Wrapping
// them here keeps the controller a callback-shape-agnostic seam and puts
// the gate in the class that owns the captured `this`, which is where it
// belongs.
_core.submitIfValid(
actionType.toStdString(), bodyJson.toStdString(), _callbacks.guard([this, actionType](std::string resultJson) {
// A successful Login is the one reply this client reads rather
// than merely displays: the token has to be installed before
Expand Down
Loading
Loading