morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
tagged.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file util/tagged.hpp
6/// @brief `Tagged<T, "Name">` — an opaque, type-safe newtype for protocol
7/// scalars that serializes transparently as its underlying `T`.
8///
9/// A protocol scalar (a pagination cursor, an event id, a job id, a bearer
10/// token) is a `T` (usually `std::string` or an integer) that must not be
11/// interchangeable with a different scalar of the same underlying type —
12/// `Tagged<std::string, "UserId">` and `Tagged<std::string, "AccountId">`
13/// carry the same wire representation but are distinct C++ types, so passing
14/// one where the other is expected is a compile error rather than a silent
15/// mix-up caught (or not) at runtime.
16///
17/// `Tag` is a `morph::detail::FixedString` non-type template parameter (the
18/// same structural string type `morph::forms::Choice` and
19/// `morph::units::NamedQuantity` use), so the identity lives in the type
20/// itself — two `Tagged<T, "X">` written identically in different
21/// translation units are one and the same type, and no companion `enum` or
22/// registry is needed to keep tags distinct.
23///
24/// **Wire.** On the morph JSON wire a `Tagged<T, Tag>` is exactly its `T`
25/// payload — `glz::meta` maps the instance to `value` alone, so it reads and
26/// writes identically to a bare `T`, and `to_json_schema` delegates to `T`'s
27/// own schema (the tag never appears on the wire or in the generated
28/// schema).
29///
30/// **Forms palette.** `Tagged` is a *required* scalar, not an
31/// optionally-empty field like `Quantity`/`Choice`/`Timestamp`: it always
32/// holds a `T` (default-constructed, never `std::nullopt`). Its `hasValue()`
33/// therefore always returns `true`, satisfying
34/// `morph::forms::EmptyCapableField` so it composes with the palette (rule
35/// vocabulary, `allRequiredEngaged`, etc.) the same way any other required
36/// field does — nothing in the palette special-cases it away like a plain,
37/// non-empty-capable scalar member would be.
38
39#include <compare>
40#include <cstddef>
41#include <glaze/glaze.hpp>
43#include <string>
44#include <string_view>
45#include <type_traits>
46#include <utility>
47
48#include "../attributes.hpp"
50
51namespace morph::util {
52
53/// @brief An opaque, type-safe newtype wrapping a protocol scalar `T`,
54/// distinguished at compile time by the string tag `Tag`.
55///
56/// @tparam T The underlying wire scalar (e.g. `std::string`, `std::int64_t`).
57/// @tparam Tag A `morph::detail::FixedString` NTTP naming this newtype (e.g.
58/// `"UserId"`); part of the C++ type, never part of the wire.
59template <typename T, morph::detail::FixedString Tag>
60struct Tagged {
61 /// @brief The underlying scalar value.
62 T value{};
63
64 /// @brief Constructs the default-valued state (`T{}`).
65 constexpr Tagged() noexcept(std::is_nothrow_default_constructible_v<T>) = default;
66
67 /// @brief Wraps @p wrapped.
68 /// @param wrapped The scalar value to tag.
69 constexpr explicit Tagged(T wrapped) noexcept(std::is_nothrow_move_constructible_v<T>)
70 : value{std::move(wrapped)} {}
71
72 /// @brief The compile-time tag naming this newtype.
73 /// @return The tag text, e.g. `"UserId"`.
74 [[nodiscard]] static constexpr std::string_view tag() noexcept { return Tag.view(); }
75
76 /// @brief Always `true`: `Tagged` is a required scalar, never empty.
77 /// @return `true`.
78 [[nodiscard]] constexpr bool hasValue() const noexcept { return true; }
79
80 /// @brief Read access to the wrapped value.
81 /// @return The underlying scalar — a reference into this `Tagged`, valid
82 /// only for as long as it is.
83 [[nodiscard]] constexpr const T& get() const noexcept MORPH_LIFETIMEBOUND { return value; }
84
85 /// @brief Unchecked access to the wrapped value, mirroring `Quantity`'s
86 /// and `Choice`'s `operator*` for consistency across the
87 /// empty-capable-field family.
88 /// @return The underlying scalar — a reference into this `Tagged`, valid
89 /// only for as long as it is.
90 [[nodiscard]] constexpr const T& operator*() const noexcept MORPH_LIFETIMEBOUND { return value; }
91
92 /// @brief Equality on the wrapped value.
93 /// @param other Tagged value to compare against (same `T`/`Tag`).
94 /// @return `true` when both wrap equal values.
95 [[nodiscard]] constexpr bool operator==(const Tagged& other) const noexcept(noexcept(value == other.value))
96 requires std::equality_comparable<T>
97 = default;
98
99 /// @brief Three-way comparison on the wrapped value, when `T` supports it.
100 /// @param other Tagged value to compare against (same `T`/`Tag`).
101 /// @return The ordering of the wrapped values.
102 [[nodiscard]] constexpr auto operator<=>(const Tagged& other) const noexcept(noexcept(value <=> other.value))
103 requires std::three_way_comparable<T>
104 = default;
105};
106
107namespace detail {
108
109/// @brief Trait: is `T` some `Tagged<...>`?
110template <typename T>
111struct IsTagged : std::false_type {};
112
113template <typename U, morph::detail::FixedString Tag>
114struct IsTagged<Tagged<U, Tag>> : std::true_type {};
115
116} // namespace detail
117
118/// @brief `true` when `T` (cvref-stripped) is a `morph::util::Tagged`.
119template <typename T>
120inline constexpr bool isTagged = detail::IsTagged<std::remove_cvref_t<T>>::value;
121
122} // namespace morph::util
123
124/// @brief On the wire a `Tagged<T, Tag>` is exactly its underlying `value` —
125/// the tag never travels; `T`'s own codec runs unchanged.
126template <typename T, morph::detail::FixedString Tag>
127struct glz::meta<morph::util::Tagged<T, Tag>> {
128 /// @brief The single wire field: the wrapped scalar.
129 static constexpr auto value = &morph::util::Tagged<T, Tag>::value;
130
131 /// @brief The tag as the schema type name, so distinct tags produce
132 /// distinctly named (if structurally identical) schema entries.
133 static constexpr std::string_view name = Tag.view();
134};
135
136namespace glz::detail {
137
138/// @brief Schema for `Tagged<T, Tag>`: identical to `T`'s own schema — the
139/// tag is a compile-time-only distinction and never surfaces in the
140/// generated JSON Schema.
141/// @tparam T The underlying wire scalar.
142/// @tparam Tag The compile-time tag (schema-invisible).
143template <typename T, morph::detail::FixedString Tag>
144struct to_json_schema<morph::util::Tagged<T, Tag>> {
145 /// @brief Emits the schema.
146 /// @tparam Opts Glaze options.
147 /// @param outSchema Schema being built.
148 /// @param defs Schema definitions.
149 template <auto Opts>
150 static void op(auto& outSchema, auto& defs) {
151 to_json_schema<T>::template op<Opts>(outSchema, defs);
152 }
153};
154
155} // namespace glz::detail
156
157/// @brief Stable shape tag for `Tagged<T, Tag>`, carrying both the tag text
158/// and the wrapped type's own shape.
159///
160/// `Tagged` is a transparent wrapper — its JSON simply *is* `T`'s — so hiding
161/// `T` behind the wrapper's name would lose real information: retyping
162/// `Tagged<std::string, "acct">` to `Tagged<std::int64_t, "acct">` genuinely
163/// changes the recorded JSON. `Inner` keeps that visible, and the tag text
164/// separates two wrappers that are structurally identical but semantically
165/// different (`"acct"` versus `"user"`), which is the swap nothing else in the
166/// system can see: the tag never travels on the wire.
167///
168/// `Tag.view()` is a `FixedString` NTTP spelled at the use site in these
169/// sources, so it is stable across compilers in a way `glz::name_v` is not.
170/// See `morph/core/payload_shape_tag.hpp`.
171/// @tparam T The wrapped scalar type.
172/// @tparam Tag The compile-time tag.
173template <typename T, morph::detail::FixedString Tag>
175 /// @brief The wrapped type, rendered inside this tag.
176 using Inner = T;
177
178 /// @brief This type's stable shape name, e.g. `"tagged.acct"`.
179 /// @return The name; the referenced storage lives for the whole process.
180 static std::string_view name() {
181 static const std::string kName = "tagged." + std::string{Tag.view()};
182 return kName;
183 }
184};
Shared compile-time, NTTP-capable fixed string.
Opt-in, compiler-independent shape tag for a type with a custom Glaze codec.
static std::string_view name()
This type's stable shape name, e.g. "tagged.acct".
Definition tagged.hpp:180
T Inner
The wrapped type, rendered inside this tag.
Definition tagged.hpp:176
Declares a stable shape tag for a type T whose JSON shape payloadShape cannot decompose.
An opaque, type-safe newtype wrapping a protocol scalar T, distinguished at compile time by the strin...
Definition tagged.hpp:60
constexpr Tagged() noexcept(std::is_nothrow_default_constructible_v< T >)=default
Constructs the default-valued state (T{}).
constexpr const T & get() const noexcept
Read access to the wrapped value.
Definition tagged.hpp:83
constexpr const T & operator*() const noexcept
Unchecked access to the wrapped value, mirroring Quantity's and Choice's operator* for consistency ac...
Definition tagged.hpp:90
constexpr auto operator<=>(const Tagged &other) const noexcept(noexcept(value<=> other.value))=default
Three-way comparison on the wrapped value, when T supports it.
constexpr bool hasValue() const noexcept
Always true: Tagged is a required scalar, never empty.
Definition tagged.hpp:78
T value
The underlying scalar value.
Definition tagged.hpp:62
constexpr bool operator==(const Tagged &other) const noexcept(noexcept(value==other.value))=default
Equality on the wrapped value.
static constexpr std::string_view tag() noexcept
The compile-time tag naming this newtype.
Definition tagged.hpp:74
Trait: is T some Tagged<...>?
Definition tagged.hpp:111
constexpr bool isTagged
true when T (cvref-stripped) is a morph::util::Tagged.
Definition tagged.hpp:120