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
3 changes: 2 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3397,7 +3397,7 @@ jobs:
# the one clang-tidy will ask.
python3 /tmp/filter-unbuilt-sources.py \
build/clang-debug/compile_commands.json /tmp/changed.diff /tmp/analysed.diff \
/tmp/tidy-db -std=c++23 -Wno-missing-include-dirs
/tmp/tidy-db -std=c++23 -Wno-missing-include-dirs -Wno-pragma-once-outside-header

# -path /tmp/tidy-db, not build/clang-debug: that is the augmented
# database, and pointing clang-tidy at it is the whole fix for
Expand All @@ -3412,6 +3412,7 @@ jobs:
-j "$(nproc)" \
-extra-arg=-std=c++23 \
-extra-arg=-Wno-missing-include-dirs \
-extra-arg=-Wno-pragma-once-outside-header \
-quiet \
< /tmp/analysed.diff \
2>&1 | tee clang-tidy-report.txt; then
Expand Down
10 changes: 6 additions & 4 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -514,15 +514,17 @@ if(MORPH_BUILD_FORMS_QML)
# stops being self-contained must fail here, not silently pass (morph#230).
set_target_properties(morph_qt_forms PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON)
target_link_libraries(morph_qt_forms INTERFACE morph Qt6::Core)
# qt_executor.hpp is listed here too, not only under morph::qt: the
# controller core includes it, and morph::qt (MORPH_BUILD_QT, which needs
# Qt WebSockets) is not part of every install that ships this header. It
# needs nothing beyond QtCore.
# qt_executor.hpp and generic_model_bridge_core.hpp are listed here too,
# 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.
target_sources(morph_qt_forms
INTERFACE
FILE_SET HEADERS
BASE_DIRS include
FILES
include/morph/qt/bridge/generic_model_bridge_core.hpp
include/morph/qt/forms/forms_controller_core.hpp
include/morph/qt/qt_executor.hpp
)
Expand Down
4 changes: 2 additions & 2 deletions codecov.yml
Original file line number Diff line number Diff line change
Expand Up @@ -314,8 +314,8 @@ component_management:
# source documents it as unreachable in practice and treats it as
# "gone" rather than asserting.
# `gui_lib/` itself is fully covered: `paste_presenter.cpp`,
# `paste_qml_bridges.cpp`, `paste_forms_controller.cpp` and both headers'
# inline bodies are at 100% lines, by `tests/test_paste_presenter.cpp` and
# `paste_qml_bridges.cpp` and both headers' inline bodies are at 100%
# lines, by `tests/test_paste_presenter.cpp` and
# `tests/test_paste_qml_bridges.cpp`.
#
# 96%, not something nearer the 98.22% ceiling, for two reasons: it leaves
Expand Down
2 changes: 1 addition & 1 deletion docs/spec/forms/forms.md
Original file line number Diff line number Diff line change
Expand Up @@ -1436,7 +1436,7 @@ not one, because only one of the two signals is universal:
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::PasteFormsController` each carry the reasoning: an unused
`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
Expand Down
15 changes: 6 additions & 9 deletions examples/bookmarks/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -455,15 +455,12 @@ types nothing.
Two pieces of glue carry their own written justification, per rule 2's "(b)
pure glue with no domain logic" clause:

- `gui::BookmarkFormsController` — this rung's copy of
`morph::qt::forms::FormsControllerCore`, composed over an injected
`Bridge&`/`IExecutor*` rather than constructing its own `LocalBackend`. The
shipped core's own composing constructor now supports this directly (the
same justification `pastebin::gui::PasteFormsController` carries), plus
one genuinely new part this rung's own controller still 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::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
23 changes: 10 additions & 13 deletions examples/bookmarks/gui_lib/bookmark_forms_controller.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,19 @@ namespace bookmarks::gui {
/// `morph::qt::forms::FormsControllerCore<Model>`
/// (`schemasJson()`/`submitIfValid()`), composed over an injected
/// `Bridge&`/`IExecutor*` instead of constructing its own
/// `LocalBackend`. The shipped core's own `(Bridge&, IExecutor*,
/// schemasJson)` constructor now supports this directly, but this
/// rung still owns a thin controller of its own: it is templated
/// `LocalBackend`. This rung still owns a thin controller of its own
/// rather than using the shipped core directly: the core is templated
/// over a *single* model, and this rung's forms span three
/// (`AuthModel`/`BookmarkModel`/`TagModel`, see "The one thing that
/// is genuinely new here" below) — `dispatch()`'s routing has no
/// equivalent on the shipped core. Pure glue, no domain logic
/// (`examples/IMPLEMENTATION.md` rule 2 justification (b)) — the
/// schema/validation/rendering machinery is untouched; only the
/// backend-wiring seam differs. Verbatim in shape from
/// `pastebin::gui::PasteFormsController`, which established it.
/// backend-wiring seam differs.
///
/// @par The one thing that is genuinely new here: routing
/// The shipped core, and pastebin's copy of it, are templates over a *single*
/// model, because rung 1 had exactly one. This rung's forms span three
/// The shipped core is a template over a *single* model. This rung's forms
/// span three
/// (`Login` on `AuthModel`, `CreateBookmark`/`EditBookmark`/`ImportBookmarks`
/// on `BookmarkModel`, `RenameTag`/`MergeTags` on `TagModel`), and
/// `BridgeHandler<Model>::executeJson` dispatches against the model type it
Expand Down Expand Up @@ -60,12 +58,11 @@ namespace bookmarks::gui {
/// `Bridge`.
///
/// @par No `fetchOptions()`
/// Deliberately absent, exactly as in `PasteFormsController`: it exists on
/// the shipped core to serve a `morph::forms::Choice<T, …>` field's combo-box
/// options, and none of this rung's DTOs declare a `Choice` field —
/// `CreateBookmark::visibility` is a plain reflected enum, not a
/// server-fetched choice. Adding an unused `fetchOptions()` would be a stub
/// with nothing to call it.
/// Deliberately absent: it exists on the shipped core to serve a
/// `morph::forms::Choice<T, …>` field's combo-box options, and none of this
/// rung's DTOs declare a `Choice` field — `CreateBookmark::visibility` is a
/// plain reflected enum, not a server-fetched choice. Adding an unused
/// `fetchOptions()` would be a stub with nothing to call it.
///
/// @par Array-typed members
/// `CreateBookmark::tags`/`EditBookmark::tags` are `std::vector<std::string>`
Expand Down
23 changes: 8 additions & 15 deletions examples/pastebin/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -227,22 +227,15 @@ must both work unchanged.
rather than dressed up: this was closed by construction, not after a
measured crash.

**Custom-GUI-element justification (`../IMPLEMENTATION.md` rule 2):** at the
time this rung was built, the shipped `morph::qt::forms::FormsControllerCore
<Model>` hardcoded its own `Bridge`/`LocalBackend`/executor internally, with
no way to compose it over `AppContext`'s `Bridge&`/`IExecutor*` — a direct
conflict with [`../TESTING.md`](../TESTING.md)'s "never construct executors
or backends themselves" presenter rule, and silently untestable in `Socket`
mode. The shipped core's own `(Bridge&, IExecutor*, schemasJson)` constructor
now supports this composition directly, closing the gap framework-side;
`gui_lib/paste_forms_controller.hpp` still owns a thin controller of its own
(this rung predates that constructor).
Pastebin's GUI still renders exclusively from `morph::forms::schemaJson<A>()`
Pastebin's GUI renders exclusively from `morph::forms::schemaJson<A>()`
through the real `MorphForms` QML module (justification (b): pure glue, no
domain logic, no hand-rolled widget) — only the backend-wiring seam is
rung-owned: a thin controller exposing the same
`schemaJson()`/`submitIfValid()`/`fetchOptions()` surface, constructed over
the `BridgeHandler<PasteModel>` `AppContext::onReady()` hands it.
domain logic, no hand-rolled widget). `gui_lib/paste_qml_bridges.hpp`'s
`FormsBridge` composes `morph::qt::forms::FormsControllerCore<PasteModel>`
directly, over the `Bridge&`/`IExecutor*` `AppContext::onReady()` hands it —
no rung-owned controller sits between them; the shipped core's
`(Bridge&, IExecutor*, schemasJson)` constructor is exactly the composition
[`../TESTING.md`](../TESTING.md)'s "never construct executors or backends
themselves" presenter rule requires.

## Required tests (from review)

Expand Down
7 changes: 4 additions & 3 deletions examples/pastebin/gui/qml/Main.qml
Original file line number Diff line number Diff line change
Expand Up @@ -90,9 +90,10 @@ ApplicationWindow {
Connections {
target: root.formsController

// The create form submits through PasteFormsController, not through
// PastePresenter, so this — not `pasteController.created` — is where a
// create's outcome arrives.
// The create form submits through FormsBridge (wrapping the shipped
// FormsControllerCore<PasteModel>), not through PastePresenter, so
// this — not `pasteController.created` — is where a create's outcome
// arrives.
function onReplyReceived(actionType, ok, payload) {
if (!ok) {
root.report(payload, true)
Expand Down
16 changes: 0 additions & 16 deletions examples/pastebin/gui_lib/paste_forms_controller.cpp

This file was deleted.

78 changes: 0 additions & 78 deletions examples/pastebin/gui_lib/paste_forms_controller.hpp

This file was deleted.

20 changes: 10 additions & 10 deletions examples/pastebin/gui_lib/paste_qml_bridges.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,27 +69,27 @@ namespace {
} // namespace

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

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

void FormsBridge::submitIfValid(const QString& actionType, const QString& bodyJson) {
// Both arms capture `this` and resolve through the executor, so neither may
// be attached bare — see this class's 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*
// `PasteFormsController::submitIfValid`, one frame further in; what this
// `FormsControllerCore::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(actionType.toStdString(), bodyJson.toStdString(),
_callbacks.guard([this, actionType](std::string resultJson) {
emit replyReceived(actionType, true, QString::fromStdString(resultJson));
}),
_callbacks.guard([this, actionType](const std::exception_ptr& err) {
emit replyReceived(actionType, false, ::morph::ladder::gui::errorText(err));
}));
_core.submitIfValid(actionType.toStdString(), bodyJson.toStdString(),
_callbacks.guard([this, actionType](const std::string& resultJson) {
emit replyReceived(actionType, true, QString::fromStdString(resultJson));
}),
_callbacks.guard([this, actionType](const std::exception_ptr& err) {
emit replyReceived(actionType, false, ::morph::ladder::gui::errorText(err));
}));
}

PasteBridge::PasteBridge(::morph::bridge::Bridge& bridge, ::morph::exec::IExecutor* executor, QObject* parent)
Expand Down
Loading
Loading