morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
inspector.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2#pragma once
3
4#include <chrono>
5#include <cstdint>
6#include <memory>
7#include <optional>
8#include <string>
9#include <string_view>
10#include <utility>
11#include <vector>
12
14#include "document.hpp"
15#include "interpreter.hpp"
16#include "value.hpp"
17
18/// @file
19/// @brief `morph::ui::Inspector`: the development inspector's read-only view of a mounted screen (spec 6 §21).
20///
21/// An inspector exists only for a screen whose session is in development mode (`DevelopmentTools::On`). Specified in
22/// `docs/spec/ui/inspector.md`.
23
24namespace morph::ui {
25
26/// @brief A mounted node as the inspector lists it.
28 /// @brief The node kind: `text`, `button`, `column`, …
29 std::string kind;
30 /// @brief The element id; empty when the node has none.
31 std::string id;
32 /// @brief The `testId`; empty when the node has none.
33 std::string testId;
34 /// @brief The node's JSON Pointer in the document.
35 std::string path;
36 /// @brief Each expression property, evaluated in the scope the node was lowered in, by its document key.
38};
39
40/// @brief A query or mutation as the inspector lists it.
42 /// @brief The declared name.
43 std::string name;
44 /// @brief Where it is declared: `screen`, `nested` (a dialog's, drawer's, switch case's or row's scope) or `app`.
45 std::string scope;
46 /// @brief Whether it is a mutation.
47 bool mutation = false;
48 /// @brief The model type.
49 std::string model;
50 /// @brief The action it runs.
51 std::string action;
52 /// @brief The cache key: model type, instance, action and canonical body; empty for an idle query and a
53 /// mutation.
54 std::string key;
55 /// @brief `idle`, `pending`, `fresh`, `stale` or `error`.
56 std::string state;
57 /// @brief How long ago the shown value arrived, on the session's clock; empty before any reply and for a
58 /// mutation.
59 std::optional<std::chrono::milliseconds> age;
60 /// @brief The last failure's text; empty when none.
61 std::string lastError;
62};
63
64/// @brief The development inspector over one mounted screen: its mounted nodes, its queries and mutations, the command
65/// lists it ran, and the reactive runtime's counters.
66///
67/// Every read returns a copy, so nothing it hands out writes the screen. The one thing it does to the screen is
68/// `refetch`, which runs the document command `{"refetch": "<query>"}` as a user's event would. Valid while the
69/// screen lives; use it on the runtime's owner.
70class Inspector {
71public:
72 /// @brief An inspector over @p screen, when its session records what an inspector reads.
73 /// @param screen The screen; it outlives the inspector.
74 /// @return The inspector; nothing when the screen's session has `DevelopmentTools::Off`, which records nothing.
75 [[nodiscard]] static std::optional<Inspector> over(Screen& screen MORPH_LIFETIMEBOUND) {
76 if (!screen._core->recording) {
77 return std::nullopt;
78 }
79 return Inspector{screen};
80 }
81
82 /// @brief The screen's document id.
83 /// @return It.
84 [[nodiscard]] std::string const& screenId() const noexcept { return _screen->id(); }
85
86 /// @brief Every node of the screen whose scope is alive, in the order it was lowered (a parent before its
87 /// children), each with its expression properties evaluated now.
88 ///
89 /// A node is listed once per scope it was lowered in, so a row's nodes are listed once per row. A switch's
90 /// `default` and a custom node's fallback are lowered in their enclosing scope, so they are listed whether or not
91 /// they are shown. An evaluation the inspector makes reports no diagnostic.
92 /// @return The nodes.
93 [[nodiscard]] std::vector<InspectedNode> nodes() const {
94 detail::ScreenCore& core = *_screen->_core;
95 Quiet const quiet{core};
96 std::vector<InspectedNode> out;
97 core.runtime().untracked([&] {
98 for (detail::ScreenCore::LoweredNode const& entry : core.loweredNodes) {
99 std::shared_ptr<detail::RtScope> const scope = entry.scope.lock();
100 std::shared_ptr<detail::Bindings const> const bindings = entry.bindings.lock();
101 if (!scope || !bindings) {
102 continue;
103 }
104 DocNode const& node = *entry.node;
105 InspectedNode inspected{.kind = node.kind,
106 .id = node.id,
107 .testId = std::string{node.name("testId")},
108 .path = node.path,
109 .properties = {}};
110 for (auto const& [key, expression] : node.props) {
111 inspected.properties.insert_or_assign(key, detail::evalIn(*scope, bindings.get(), expression));
112 }
113 out.push_back(std::move(inspected));
114 }
115 });
116 return out;
117 }
118
119 /// @brief Every query and mutation of the screen's live scopes, the screen's own first, then those of the app
120 /// document, with their state now.
121 ///
122 /// A query is `idle` with no cache entry (its `when` is not `true`, its alias is unbound or its body is `null`),
123 /// and with an entry that has neither a value nor a request; `pending` while a request is in flight; `error`
124 /// when the last request failed; `stale` when its entry was invalidated or its value is at least `staleTime`
125 /// old; `fresh` otherwise. A mutation is `pending` while a run is in flight or queued, `error` when its last run
126 /// failed, `fresh` once a run succeeded, and `idle` before any.
127 /// @return The queries and mutations.
128 [[nodiscard]] std::vector<InspectedQuery> queries() const {
129 detail::ScreenCore& core = *_screen->_core;
130 std::vector<InspectedQuery> out;
131 core.runtime().untracked([&] {
132 for (detail::RtScope const* const scope : core.scopes) {
133 collect(*scope, scope == _screen->_scope.get() ? "screen" : "nested", out);
134 }
135 if (detail::RtScope const* const app = core.session.appScope()) {
136 collect(*app, "app", out);
137 }
138 });
139 return out;
140 }
141
142 /// @brief The latest command lists the screen ran, oldest first, at most `detail::kCommandLogSize`.
143 /// @return Each list's event, its commands and its batch.
144 [[nodiscard]] std::vector<CommandLogEntry> commandLog() const {
145 auto const& log = _screen->_core->commandLog;
146 return {log.begin(), log.end()};
147 }
148
149 /// @brief The reactive runtime's counters: flushes, Effects per flush, batches and misuses.
150 /// @return They.
151 [[nodiscard]] reactive::RuntimeStats counters() const { return _screen->runtime().stats(); }
152
153 /// @brief Refetches a query by running the document command `{"refetch": "<query>"}` in the scope that declares
154 /// it, as a user's event runs a command list: the same path, the same batch, the same log entry.
155 /// @param query The query's name, as `queries()` lists it.
156 /// @return Whether a live scope of the screen declares a query of that name.
157 bool refetch(std::string_view query) {
158 detail::ScreenCore& core = *_screen->_core;
159 for (detail::RtScope* const scope : core.scopes) {
160 auto const found = scope->names.find(query);
161 if (found == scope->names.end() || found->second.category != detail::NameCategory::Query) {
162 continue;
163 }
164 Commands const commands{
165 Command{.kind = CommandKind::Refetch, .path = "/inspector/0", .target = std::string{query}}};
166 detail::CommandCtx const ctx{.scope = scope->shared_from_this(),
167 .bindings = std::make_shared<detail::Bindings>(),
168 .cancelClose = std::make_shared<bool>(false),
169 .boundary = nullptr};
170 core.session.asUser([&] { core.runtime().batch([&] { core.runList(commands, ctx, {}); }); });
171 return true;
172 }
173 return false;
174 }
175
176private:
177 /// Turns off the screen's diagnostics while the inspector evaluates.
178 class Quiet {
179 public:
180 explicit Quiet(detail::ScreenCore& core) : _core{&core}, _was{core.inspecting} { core.inspecting = true; }
181 ~Quiet() { _core->inspecting = _was; }
182 Quiet(Quiet const&) = delete;
183 Quiet& operator=(Quiet const&) = delete;
184 Quiet(Quiet&&) = delete;
185 Quiet& operator=(Quiet&&) = delete;
186
187 private:
188 detail::ScreenCore* _core;
189 bool _was;
190 };
191
192 [[nodiscard]] static std::string text(Value const& value) {
193 std::string const* const string = value.asString();
194 return string == nullptr ? std::string{} : *string;
195 }
196
197 void collect(detail::RtScope const& scope, std::string const& where, std::vector<InspectedQuery>& out) const {
198 detail::ScreenCore const& core = *_screen->_core;
199 std::chrono::milliseconds const now = core.session.now();
200 for (auto const& query : scope.queries) {
201 InspectedQuery inspected{.name = query->decl().name,
202 .scope = where,
203 .mutation = false,
204 .model = query->typeId(),
205 .action = query->decl().action,
206 .key = {},
207 .state = "idle",
208 .age = std::nullopt,
209 .lastError = {}};
210 if (std::shared_ptr<detail::CacheEntry> const& entry = query->entry()) {
211 inspected.key = entry->key;
212 inspected.lastError = entry->error.peek();
213 if (entry->fetchedAt) {
214 inspected.age = now - *entry->fetchedAt;
215 }
216 bool const old = inspected.age && *inspected.age >= std::chrono::milliseconds{query->decl().staleTime};
217 if (entry->pending.peek()) {
218 inspected.state = "pending";
219 } else if (!inspected.lastError.empty()) {
220 inspected.state = "error";
221 } else if (!entry->hasValue.peek()) {
222 inspected.state = "idle";
223 } else {
224 inspected.state = entry->stale || old ? "stale" : "fresh";
225 }
226 }
227 out.push_back(std::move(inspected));
228 }
229 for (auto const& mutation : scope.mutations) {
230 InspectedQuery inspected{.name = mutation->decl().name,
231 .scope = where,
232 .mutation = true,
233 .model = mutation->typeId(),
234 .action = mutation->decl().action,
235 .key = {},
236 .state = "idle",
237 .age = std::nullopt,
238 .lastError = text(mutation->error())};
239 if (mutation->pending()) {
240 inspected.state = "pending";
241 } else if (!inspected.lastError.empty()) {
242 inspected.state = "error";
243 } else if (!mutation->result().isNull()) {
244 inspected.state = "fresh";
245 }
246 out.push_back(std::move(inspected));
247 }
248 }
249
250 explicit Inspector(Screen& screen) : _screen{&screen} {}
251
252 Screen* _screen;
253};
254
255} // namespace morph::ui
Screen screen(Screen::Builder builder)
A screen with its id as a template argument, as spec 5 §11 writes it.
Definition author.hpp:4556
decltype(auto) untracked(F &&fn) const
Runs fn with tracking off: reads inside it subscribe nothing.
Definition runtime.hpp:136
std::vector< InspectedNode > nodes() const
Every node of the screen whose scope is alive, in the order it was lowered (a parent before its child...
Definition inspector.hpp:93
static std::optional< Inspector > over(Screen &screen)
An inspector over screen, when its session records what an inspector reads.
Definition inspector.hpp:75
bool refetch(std::string_view query)
Refetches a query by running the document command {"refetch": "<query>"} in the scope that declares i...
std::string const & screenId() const noexcept
The screen's document id.
Definition inspector.hpp:84
std::vector< CommandLogEntry > commandLog() const
The latest command lists the screen ran, oldest first, at most detail::kCommandLogSize.
std::vector< InspectedQuery > queries() const
Every query and mutation of the screen's live scopes, the screen's own first, then those of the app d...
reactive::RuntimeStats counters() const
The reactive runtime's counters: flushes, Effects per flush, batches and misuses.
A mounted screen: its scopes, its view mounted through Mounted, and what its host asks of it.
One document scope at run time: the screen, a dialog's or drawer's content, a switch case or a row.
std::map< std::string, NameRef, std::less<> > names
Its names.
One mounted screen's interpreter: its document, diagnostics, slots and preferences,...
reactive::Runtime & runtime() const noexcept
The runtime.
bool inspecting
Set while the inspector evaluates, so that an expression error it meets is not reported.
The UI document (spec 5 §3): its model — declarations, commands, view nodes, components and customiza...
std::vector< Command > Commands
A command list, run as one batch.
Definition document.hpp:239
@ Refetch
{"refetch": "q"}
Definition document.hpp:246
The UI document interpreter: Interpreter::mount turns a loaded Document into reactive state and a vie...
Value evalIn(RtScope const &scope, Bindings const *bindings, ExprPtr const &expr)
Evaluates an expression in a scope.
morph::reactive::Runtime: the owner, batching and flush scheduling of a signal graph.
What a Runtime has counted since it was made: its flushes, the Effects they ran, its batches and the ...
Definition runtime.hpp:42
One command. Which members a kind uses is listed with each member.
Definition document.hpp:271
A view node of the document (spec 5 §9), or a structured part of one (a table column,...
Definition document.hpp:394
std::string path
The node's JSON Pointer.
Definition document.hpp:400
std::string_view name(std::string_view key) const
A plain string property.
Definition document.hpp:455
std::string kind
The node kind (text, table, …), or for a part its part kind (column, tab, case, …).
Definition document.hpp:396
std::string id
The element id; empty when none is given.
Definition document.hpp:398
std::map< std::string, ExprPtr, std::less<> > props
Expression properties, by key.
Definition document.hpp:402
A mounted node as the inspector lists it.
Definition inspector.hpp:27
std::string kind
The node kind: text, button, column, …
Definition inspector.hpp:29
std::string id
The element id; empty when the node has none.
Definition inspector.hpp:31
std::string testId
The testId; empty when the node has none.
Definition inspector.hpp:33
ValueRecord properties
Each expression property, evaluated in the scope the node was lowered in, by its document key.
Definition inspector.hpp:37
std::string path
The node's JSON Pointer in the document.
Definition inspector.hpp:35
A query or mutation as the inspector lists it.
Definition inspector.hpp:41
std::string scope
Where it is declared: screen, nested (a dialog's, drawer's, switch case's or row's scope) or app.
Definition inspector.hpp:45
std::string lastError
The last failure's text; empty when none.
Definition inspector.hpp:61
bool mutation
Whether it is a mutation.
Definition inspector.hpp:47
std::optional< std::chrono::milliseconds > age
How long ago the shown value arrived, on the session's clock; empty before any reply and for a mutati...
Definition inspector.hpp:59
std::string state
idle, pending, fresh, stale or error.
Definition inspector.hpp:56
std::string model
The model type.
Definition inspector.hpp:49
std::string name
The declared name.
Definition inspector.hpp:43
std::string action
The action it runs.
Definition inspector.hpp:51
std::string key
The cache key: model type, instance, action and canonical body; empty for an idle query and a mutatio...
Definition inspector.hpp:54
Where a command list runs: its scope, the names bound there, and what it may answer.
A node as it was lowered, which the inspector lists while its scope lives.
std::weak_ptr< Bindings const > bindings
The names bound where it was lowered.
std::weak_ptr< RtScope > scope
The scope it was lowered in.
The UI document's typed values (ui::Value), its types (ui::Type), their JSON form,...
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Definition value.hpp:129