morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
model_key.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <charconv>
5#include <concepts>
6#include <stdexcept>
7#include <string>
8#include <string_view>
9#include <system_error>
10#include <type_traits>
11#include <utility>
12
13/// @file
14/// Primary keys for model instances.
15///
16/// A model type declares itself *keyed* by exposing a nested `PrimaryKey` type
17/// alias; the alias is detected structurally (a `requires`-expression), never by
18/// inheritance or a marker base — the same detection style
19/// `morph::views::ViewTraits` uses for a view's `kind`/`query` members. A model
20/// without the alias is unkeyed and behaves exactly as it always has.
21///
22/// Actions say *which of their fields carries* that key via `BRIDGE_KEY_FROM`,
23/// or — for an action that creates the entity rather than naming it — which
24/// field of their *result* establishes it, via `BRIDGE_KEY_FROM_RESULT`. Actions
25/// with neither declaration are keyless and run against whichever instance the
26/// handler is already attached to; that is the common case.
27///
28/// Keys travel the wire as strings (`wire::Envelope::primary`) regardless of
29/// their C++ type, so the directory in `RemoteServer` needs exactly one map type
30/// rather than one per key type. See docs/spec/core/shared_instances.md.
31
32namespace morph::model {
33
34/// @brief Key types that are themselves a wire-encodable scalar.
35///
36/// Restricted to integral types and `std::string` because a key must round-trip
37/// losslessly through the wire's string encoding and be usable as a map key.
38/// `bool` is excluded: it carries one bit of identity, which is never a
39/// meaningful primary key and is far more likely to be a mistake.
40template <typename K>
41concept RawModelKey =
42 (std::integral<K> && !std::same_as<std::remove_cv_t<K>, bool>) || std::same_as<std::remove_cv_t<K>, std::string>;
43
44/// @brief Key types that *wrap* a `RawModelKey` — the ladder's strong ids.
45///
46/// `examples/IMPLEMENTATION.md` rule 3 requires entity identity to be a
47/// per-entity strong id type (`struct PasteId`, `struct OptionId`) exposing
48/// `hasValue()`, so that it joins the forms palette as an empty-capable field.
49/// Such a type is not integral and is not `std::string`, so before this it
50/// satisfied no arm of `ModelKey` at all — and a rung obeying rule 3 could not
51/// use `BRIDGE_MODEL_KEY`/`BRIDGE_KEY_FROM`, the very macros the framework
52/// provides for keying. Three rungs hand-wrote the traits instead, each
53/// re-stating the `*id` unwrapping convention the macro exists to hide.
54///
55/// The shape is deduced structurally rather than declared, matching how a
56/// keyed model is detected: any type with `hasValue()` and an `operator*`
57/// yielding a `RawModelKey`, and constructible back from that raw value so the
58/// key round-trips from the wire. Both ladder spellings qualify — an
59/// `std::optional`-backed id whose `operator*` returns a reference, and a
60/// sentinel-backed one (`0` means empty) whose `operator*` returns by value.
61template <typename K>
62concept WrappedModelKey = !RawModelKey<K> && requires(const K& key) {
63 { key.hasValue() } -> std::convertible_to<bool>;
64 { *key };
65 requires RawModelKey<std::remove_cvref_t<decltype(*key)>>;
66 requires requires(std::remove_cvref_t<decltype(*key)> raw) { K{std::move(raw)}; };
67};
68
69/// @brief Key types a model may declare as its `PrimaryKey`.
70///
71/// Either a wire-encodable scalar (`RawModelKey`) or a strong id wrapping one
72/// (`WrappedModelKey`).
73template <typename K>
75
76namespace detail {
77
78/// @brief The scalar a `WrappedModelKey` wraps.
79/// @tparam K The wrapping strong id type.
80template <WrappedModelKey K>
81using UnwrappedKeyOf = std::remove_cvref_t<decltype(*std::declval<const K&>())>;
82
83} // namespace detail
84
85/// @brief The key type of a model, when one has been declared *for* it.
86///
87/// Specialised by `BRIDGE_MODEL_KEY` (and `BRIDGE_MODEL_KEY_FROM_RESULT`);
88/// `BRIDGE_KEY_FROM` emits only `ActionKeyTraits` and deliberately leaves this
89/// alone, since the model's key type is already established by its one
90/// `BRIDGE_MODEL_KEY` line. The primary template is deliberately empty: a model
91/// with neither this specialisation nor a nested alias is simply unkeyed.
92/// @tparam Model Concrete model type.
93template <typename Model>
95
96namespace detail {
97
98/// @brief Satisfied by a model that names its own key with a nested alias.
99template <typename M>
100concept SelfDeclaredKey = requires { typename M::PrimaryKey; } && ModelKey<typename M::PrimaryKey>;
101
102/// @brief Satisfied by a model whose key was deduced from a keyed action.
103template <typename M>
104concept DeducedKey =
105 requires { typename ModelKeyTraits<M>::PrimaryKey; } && ModelKey<typename ModelKeyTraits<M>::PrimaryKey>;
106
107} // namespace detail
108
109/// @brief Satisfied by model types that have a primary key, however it was named.
110///
111/// Two ways in, and neither requires touching the model's own class body beyond
112/// the first: a nested `PrimaryKey` alias, or a `BRIDGE_MODEL_KEY` declaration,
113/// which deduces the type from the model field it is given
114/// (`MemberTypeOf<decltype(MEMBER)>`). Following
115/// `morph::forms`' standing rule — *infer by default, declare to override* — a
116/// nested alias wins when both are present, which is what lets a model whose
117/// key type differs from the field's type (an `int` column keyed as a
118/// `std::string`, say) state that explicitly.
119template <typename M>
120concept KeyedModel = detail::SelfDeclaredKey<M> || detail::DeducedKey<M>;
121
122namespace detail {
123
124/// @brief Picks the nested alias when present, else the deduced one.
125template <typename M>
126struct KeyTypeOf {
127 /// @brief The resolved key type.
129};
130
131template <SelfDeclaredKey M>
132struct KeyTypeOf<M> {
133 /// @brief The resolved key type — the model's own alias takes precedence.
134 using type = M::PrimaryKey;
135};
136
137} // namespace detail
138
139/// @brief The primary key type of a keyed model.
140/// @tparam M Keyed model type.
141template <KeyedModel M>
142using PrimaryKeyOf = detail::KeyTypeOf<M>::type;
143
144/// @brief Encodes a primary key as its canonical wire string.
145///
146/// Integral keys are decimal; `std::string` keys pass through unchanged; a
147/// strong id encodes as whatever it wraps, so it shares a directory entry with
148/// the raw key of the same value. The encoding is total for every *engaged*
149/// key — each has exactly one representation — so two clients naming the same
150/// key always land on the same directory entry.
151/// @tparam K Key type satisfying `ModelKey`.
152/// @param key Key value to encode.
153/// @return The canonical string form of @p key.
154/// @throws std::runtime_error if @p key is a strong id with no value. Such an
155/// id names no instance, and encoding it as `""` or `"0"` would route
156/// every caller holding an unset id to a single shared instance.
157template <ModelKey K>
158[[nodiscard]] std::string keyToString(const K& key) {
159 if constexpr (WrappedModelKey<K>) {
160 // An empty strong id names no instance. Unwrapping it is undefined
161 // behaviour (`operator*` on a disengaged optional), and encoding it as
162 // "" or "0" would silently route every caller holding an unset id to
163 // one shared instance -- the worst possible failure, because it looks
164 // like it worked. `BridgeHandler` turns a throw here into a rejected
165 // `Completion`, or for `attach` a logged refusal that rejects the
166 // handler's calls, so this surfaces as an error the caller can see.
167 if (!key.hasValue()) {
168 throw std::runtime_error("primary key is empty: a strong id with no value names no model instance");
169 }
170 return keyToString(*key);
171 } else if constexpr (std::same_as<std::remove_cv_t<K>, std::string>) {
172 return key;
173 } else {
174 return std::to_string(key);
175 }
176}
177
178/// @brief Decodes a primary key from its canonical wire string.
179///
180/// A strong id is reconstructed by decoding what it wraps and rewrapping it,
181/// so `keyFromString<PasteId>(keyToString(id)) == id` for any engaged `id`.
182/// @tparam K Key type satisfying `ModelKey`.
183/// @param text Canonical string form, as produced by `keyToString`.
184/// @return The decoded key.
185/// @throws std::runtime_error if @p text is not a valid encoding of a `K`
186/// (non-numeric text, trailing garbage, or a value out of range). A key
187/// that cannot be decoded is a protocol error, not a value to clamp:
188/// silently yielding 0 would route the caller to the wrong instance.
189template <ModelKey K>
190[[nodiscard]] K keyFromString(std::string_view text) {
191 if constexpr (WrappedModelKey<K>) {
193 } else if constexpr (std::same_as<std::remove_cv_t<K>, std::string>) {
194 return std::string{text};
195 } else {
196 K value{};
197 // from_chars is a [first, last) pointer API; a string_view's data()+size()
198 // is the only way to express its end, and is exactly what the standard
199 // intends here.
200 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-pointer-arithmetic)
201 auto [ptr, errc] = std::from_chars(text.data(), text.data() + text.size(), value);
202 const auto* const last = text.data() + text.size(); // NOLINT(cppcoreguidelines-pro-bounds-pointer-arithmetic)
203 if (errc != std::errc{} || ptr != last) {
204 throw std::runtime_error("invalid primary key encoding: '" + std::string{text} + "'");
205 }
206 return value;
207 }
208}
209
210/// @brief Declares where an action's model key comes from.
211///
212/// The primary template is the *keyless* case, which is the default and the
213/// common one: such an action says nothing about identity and runs against
214/// whichever instance its handler is already attached to. Specialise via
215/// `BRIDGE_KEY_FROM` or `BRIDGE_KEY_FROM_RESULT` rather than by hand.
216/// @tparam Action Concrete action type.
217template <typename Action>
219 /// @brief Whether this action carries or establishes its model's key.
220 static constexpr bool hasKey = false;
221
222 /// @brief Whether the key comes from the action's result rather than its payload.
223 static constexpr bool fromResult = false;
224};
225
226namespace detail {
227
228/// @brief The type of the data member a pointer-to-member points at.
229///
230/// Lets `BRIDGE_KEY_FROM` deduce the model's key type from the action field it
231/// is handed, so the key type is never written out twice.
232template <typename T>
233struct MemberType;
234
235template <typename V, typename C>
236struct MemberType<V C::*> {
237 /// @brief The member's own type.
238 using type = V;
239};
240
241/// @brief Convenience alias for `MemberType<T>::type`.
242template <typename T>
243using MemberTypeOf = MemberType<std::remove_cv_t<T>>::type;
244
245/// @brief Satisfied by actions whose key is carried in the action payload.
246template <typename A>
248
249/// @brief Satisfied by actions whose key is established by the action's result.
250template <typename A>
252
253} // namespace detail
254
255} // namespace morph::model
256
257// NOLINTBEGIN(cppcoreguidelines-macro-usage) — declaration macros are the intended public API,
258// matching BRIDGE_REGISTER_MODEL/ACTION in registry.hpp: they must emit a template
259// specialisation at global scope, which no function template can do.
260
261/// @brief Declares that action `A` is the one that defines model `M`'s primary key.
262///
263/// One line does both jobs, so the model's own class body needs to say nothing
264/// about keys: the key *type* is deduced from `MEMBER`'s type (defining
265/// `ModelKeyTraits<M>`), and `A` is recorded as an action that carries it
266/// (defining `ActionKeyTraits<A>`).
267///
268/// Executing such an action on a shareable handler attaches — or re-points —
269/// that handler to the instance holding the named key, constructing one only if
270/// no instance holds it yet. Every *keyless* action on that handler afterwards
271/// lands on the same instance, which is what makes the common case free of
272/// ceremony:
273///
274/// ```cpp
275/// BRIDGE_KEY_FROM(AccountModel, LoadAccount, &LoadAccount::id);
276///
277/// BridgeHandler<AccountModel, AllowShared> first{bridge, gui}, second{bridge, gui};
278/// first .execute(LoadAccount{.id = 32}); // constructs the instance for 32
279/// second.execute(LoadAccount{.id = 32}); // attaches to it; constructs nothing
280/// first .execute(Deposit{.amount = 100}); // keyless -> instance 32
281/// second.execute(GetBalance{}); // keyless -> instance 32, sees the 100
282/// ```
283///
284/// `MEMBER` is a pointer-to-data-member of `A` (e.g. `&LoadAccount::id`) whose
285/// type satisfies `morph::model::ModelKey`. Must appear at global scope; a
286/// header included by several translation units is fine, like the other
287/// `BRIDGE_REGISTER_*` macros — this macro emits only explicit specialisations,
288/// which `[basic.def.odr]` permits in multiple translation units when the
289/// definitions are token-identical. See docs/spec/core/registry.md, "Header
290/// placement is legal".
291// clang-format off -- public macro surface; see CONTRIBUTING.md, "Formatting/linting".
292#define BRIDGE_MODEL_KEY(M, A, MEMBER) \
293 template <> \
294 struct morph::model::ActionKeyTraits<A> { \
295 static constexpr bool hasKey = true; \
296 static constexpr bool fromResult = false; \
297 static std::string key(const A& action) { return morph::model::keyToString(action.*MEMBER); } \
298 }; \
299 template <> \
300 struct morph::model::ModelKeyTraits<M> { \
301 using PrimaryKey = morph::model::detail::MemberTypeOf<decltype(MEMBER)>; \
302 }
303// clang-format on
304
305/// @brief Declares that action `A` also carries its model's primary key in `MEMBER`.
306///
307/// The companion to `BRIDGE_MODEL_KEY`, for the *other* actions that name the
308/// same entity — `CloseAccount{.id = ...}` alongside `GetAccount{.id = ...}`.
309/// It records only that `A` carries the key; the key's type has already been
310/// established by the model's one `BRIDGE_MODEL_KEY` line, and an explicit
311/// specialisation cannot be repeated.
312///
313/// Must appear at global scope; a header included by several translation units
314/// is fine (only explicit specialisations are emitted — see
315/// docs/spec/core/registry.md, "Header placement is legal").
316// clang-format off -- public macro surface; see CONTRIBUTING.md, "Formatting/linting".
317#define BRIDGE_KEY_FROM(A, MEMBER) \
318 template <> \
319 struct morph::model::ActionKeyTraits<A> { \
320 static constexpr bool hasKey = true; \
321 static constexpr bool fromResult = false; \
322 static std::string key(const A& action) { return morph::model::keyToString(action.*MEMBER); } \
323 }
324// clang-format on
325
326/// @brief Declares that action `A`'s *result* establishes its model's primary key.
327///
328/// For actions that create the entity rather than name it: the key is not in the
329/// request, it is generated and returned, exactly as a database insert returns
330/// its generated primary key. `MEMBER` is a pointer-to-data-member of `A`'s
331/// result type (e.g. `&AccountInfo::id`).
332///
333/// Must appear at global scope; a header included by several translation units
334/// is fine (only explicit specialisations are emitted — see
335/// docs/spec/core/registry.md, "Header placement is legal").
336// clang-format off -- public macro surface; see CONTRIBUTING.md, "Formatting/linting".
337#define BRIDGE_MODEL_KEY_FROM_RESULT(M, A, MEMBER) \
338 template <> \
339 struct morph::model::ActionKeyTraits<A> { \
340 static constexpr bool hasKey = true; \
341 static constexpr bool fromResult = true; \
342 template <typename R> \
343 static std::string keyOfResult(const R& result) { \
344 return morph::model::keyToString(result.*MEMBER); \
345 } \
346 }; \
347 template <> \
348 struct morph::model::ModelKeyTraits<M> { \
349 using PrimaryKey = morph::model::detail::MemberTypeOf<decltype(MEMBER)>; \
350 }
351// clang-format on
352
353/// @brief Declares that action `A`'s result also establishes its model's key.
354///
355/// The companion to `BRIDGE_MODEL_KEY_FROM_RESULT`, for a second creating
356/// action on a model whose key type is already established.
357///
358/// Must appear at global scope; a header included by several translation units
359/// is fine (only explicit specialisations are emitted — see
360/// docs/spec/core/registry.md, "Header placement is legal").
361// clang-format off -- public macro surface; see CONTRIBUTING.md, "Formatting/linting".
362#define BRIDGE_KEY_FROM_RESULT(A, MEMBER) \
363 template <> \
364 struct morph::model::ActionKeyTraits<A> { \
365 static constexpr bool hasKey = true; \
366 static constexpr bool fromResult = true; \
367 template <typename R> \
368 static std::string keyOfResult(const R& result) { \
369 return morph::model::keyToString(result.*MEMBER); \
370 } \
371 }
372// clang-format on
373
374// NOLINTEND(cppcoreguidelines-macro-usage)
Satisfied by model types that have a primary key, however it was named.
Key types a model may declare as its PrimaryKey.
Definition model_key.hpp:74
Key types that are themselves a wire-encodable scalar.
Definition model_key.hpp:41
Key types that wrap a RawModelKey — the ladder's strong ids.
Definition model_key.hpp:62
detail::KeyTypeOf< M >::type PrimaryKeyOf
The primary key type of a keyed model.
K keyFromString(std::string_view text)
Decodes a primary key from its canonical wire string.
std::string keyToString(const K &key)
Encodes a primary key as its canonical wire string.
Declares where an action's model key comes from.
static constexpr bool fromResult
Whether the key comes from the action's result rather than its payload.
static constexpr bool hasKey
Whether this action carries or establishes its model's key.
The key type of a model, when one has been declared for it.
Definition model_key.hpp:94