Skip to content

qt/forms: multi-model bridge routing core, retrofitting bookmarks' FormsBridge - #835

Open
Yaraslaut wants to merge 3 commits into
masterfrom
feature/827-multimodel-bridge-core
Open

Yaraslaut wants to merge 3 commits into
masterfrom
feature/827-multimodel-bridge-core

Conversation

@Yaraslaut

Copy link
Copy Markdown
Member

Summary

  • Adds morph::qt::bridge::MultiModelBridgeCore<Sharing, Model...>, a variadic sibling of GenericModelBridgeCore<Model, Sharing> for a rung whose forms span more than one registered model. It composes one GenericModelBridgeCore<Model, Sharing> per Model in the pack and routes a submitted action-type id to whichever one serves it via a new existence-check (BridgeHandler::servesAction / GenericModelBridgeCore::servesAction, backed by ActionExecuteRegistry::contains<Sharing>) — the routing is derived from the same ActionExecuteRegistry BRIDGE_REGISTER_ACTION already populates, not a hand-written actionType -> Model table.
  • Adds morph::qt::forms::MultiModelFormsControllerCore<Sharing, Model...>, the forms-facing facade mirroring FormsControllerCore's schemasJson()/submitIfValid()/fetchOptions() surface.
  • Retrofits examples/bookmarks' FormsBridge to compose the new core directly (AuthModel, BookmarkModel, TagModel), deleting the hand-rolled BookmarkFormsController entirely — the example proof this seam works end-to-end.
  • Extracts the owning-constructor's private pool/executor/backend bundle (previously duplicated in GenericModelBridgeCore) into a shared morph::qt::bridge::detail::OwnedLocalBridge.

Kanban's ProjectAdminPresenter is deliberately not retrofitted: its routing has per-action side effects (re-decoding a body for a typed signal, installing a session, redacting a token) that a generic routing table can't serve without also templating those effects — confirmed by reading it side-by-side with the deleted BookmarkFormsController::dispatch, per the issue's own named contingency for this case.

Closes #827

Verification

  • Full repo test suite: 2005/2005 passing (AppleClang 17, -Weverything -Werror), including bookmarks' 130-case suite (all six actions' routing + the unrouted-action error path) and a new dedicated test_multi_model_bridge_core.cpp covering the core directly.
  • Doxygen docs build (MORPH_BUILD_DOCUMENTATION=ON, WARN_AS_ERROR = FAIL_ON_WARNINGS) passes clean.
  • Reviewed via /simplify (4-angle pass, twice — per-phase and whole-branch) and scoped /code-review (medium per commit, high-effort whole-branch) agents; all real findings addressed (a move-vs-copy inefficiency was fixed by composing GenericModelBridgeCore instead of duplicating its completion-wiring; a missing noteRegistryRead call in the new contains<Sharing>; two doc-accuracy issues in docs/spec/forms/forms.md; one pre-existing doc-precision gap in FormsBridge's own comment, fixed in its own commit).

Test plan

  • ctest full suite green (2005/2005)
  • examples/bookmarks suite green (130/130), including routing + unrouted-action cases
  • New multi_model_bridge_core / forms_controller_core unit tests green
  • Doxygen docs build green

🤖 Generated with Claude Code

https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc

@Yaraslaut
Yaraslaut force-pushed the feature/827-multimodel-bridge-core branch 4 times, most recently from e6e591b to d6eed40 Compare September 26, 2026 19:26
Yaraslaut and others added 3 commits September 26, 2026 21:57
…elBridgeCore

Two example rungs (bookmarks' BookmarkFormsController, kanban's
ProjectAdminPresenter) independently hand-write an actionType -> handler
routing table because GenericModelBridgeCore<Model, Sharing> and its forms
facade FormsControllerCore are single-Model templates by construction and
cannot express dispatch across several registered models.

Add morph::qt::bridge::MultiModelBridgeCore<Sharing, Model...>: it composes
one GenericModelBridgeCore<Model, Sharing> per Model in the pack and routes a
submitted actionType to whichever one serves it. The routing is derived, not
declared: GenericModelBridgeCore::servesAction (added alongside its own
BridgeHandler::servesAction, backed by a new
ActionExecuteRegistry::contains<Sharing> existence check) answers "does this
Model recognise this action" directly from the same ActionExecuteRegistry
BRIDGE_REGISTER_ACTION already populates, so nothing here re-states a mapping
that already exists as data. Models are tried in pack order via a std::apply
fold over a tuple of GenericModelBridgeCore instances, so the one Model that
matches dispatches through its own already-correct execute() rather than a
second, hand-duplicated copy of its completion-wiring. An action registered
on more than one Model is a configuration bug, asserted in debug builds
rather than resolved silently. An unrouted action resolves onError directly
with "no model in this client serves action '<id>'" -- the exact wording
both hand-written routers already agreed on independently.

morph::qt::forms::MultiModelFormsControllerCore<Sharing, Model...> is the
forms-facing facade over it, mirroring FormsControllerCore's
schemasJson()/submitIfValid()/fetchOptions() surface.

Extract the owning-constructor's private pool/executor/backend bundle,
previously duplicated verbatim in GenericModelBridgeCore, into a shared
morph::qt::bridge::detail::OwnedLocalBridge used by both cores -- a pure
refactor with no behavior change, so the framework does not reintroduce the
same "written twice" shape this change exists to remove from examples/.

Kanban's ProjectAdminPresenter is deliberately not retrofitted here (see the
following commit) -- its routing has per-action side effects (re-decoding a
body for a typed signal, installing a session, redacting a token) that a
generic routing table cannot serve without also templating those effects.
Towards morph#827 (the retrofit that proves this core lands next).

Claude-Session: https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
…ollerCore

BookmarkFormsController hand-rolled exactly what morph::qt::forms::
MultiModelFormsControllerCore (previous commit) now provides generically:
one BridgeHandler per model (AuthModel, BookmarkModel, TagModel) and an
actionType -> handler routing table, plus a try/catch converting the
routing throw into the same onError shape every other failure takes.

Delete BookmarkFormsController entirely. FormsBridge now composes
MultiModelFormsControllerCore<NoSharing, AuthModel, BookmarkModel, TagModel>
directly, over the Bridge&/IExecutor* AppContext::onReady() hands it --
same pattern examples/pastebin's FormsBridge already uses for
FormsControllerCore<PasteModel> (#828). The rung's existing suite
(test_bookmark_qml_bridges.cpp) already covered all six actions' routing and
the unrouted-action error message end-to-end through FormsBridge, so it
proves the auto-derived routing reproduces the hand-written table's
behaviour unchanged -- verified: same 837 assertions across 130 test cases
pass unmodified, including the six-action routing case and the
CreateBookmarks/ListSharedFeed unrouted-action case.

Update every other place that named BookmarkFormsController by symbol
(bookmark_schemas.hpp, main_wasm.cpp, polls' and kanban's own doc comments
comparing their shape to it, the DynamicForm choiceless-controller test) to
point at FormsBridge/MultiModelFormsControllerCore instead, so no comment is
left describing a class that no longer exists. Kanban's ProjectAdminPresenter
comment is updated to explain, rather than merely note, why its own routing
stays hand-written: its per-action side effects (re-decoding CreateProject's
body for a typed signal, installing a session on Login, redacting a token)
are tangled into the routing in a way MultiModelBridgeCore's generic
dispatch cannot serve without also templating those effects -- confirmed by
reading ProjectAdminPresenter::submitForm and BookmarkFormsController::
dispatch side by side, per the triage's own named contingency for this case.

Closes #827

Claude-Session: https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
… doc

FormsBridge's "Why this class holds a CallbackScope" comment claimed every
submitIfValid reply resolves through the executor, never inline -- true for
a routed action's Completion, but not for an unrouted one: dispatch through
morph::qt::forms::MultiModelFormsControllerCore reports "no model in this
client serves action" via onError synchronously, on submitIfValid's own call
frame, since no model's Completion was ever created. Pre-existing behaviour
(the deleted BookmarkFormsController's try/catch around a throwing router
had the same synchronous-onError shape for this one case) that the doc
comment never carved out; noted while touching this file for #827's
MultiModelFormsControllerCore retrofit.

Claude-Session: https://claude.ai/code/session_018fEUahMFF32wQLiWjbsfkc
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Signed-off-by: Yaraslau Tamashevich <yaraslau.tamashevich@gmail.com>
@Yaraslaut
Yaraslaut force-pushed the feature/827-multimodel-bridge-core branch from d6eed40 to f45a4d4 Compare September 26, 2026 20:13
@codecov

codecov Bot commented Sep 26, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

qt/forms: BookmarkFormsController hand-rolls multi-model action routing that no core template supports

1 participant