morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
schema_name.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4
5/// @file forms/detail/schema_name.hpp
6/// @brief Compile-time `$defs` keys for the forms layer's field wrappers.
7///
8/// glaze keys a schema's `$defs` block by `glz::name_v<T>` and populates each
9/// entry **once**: `auto& def = defs[name_v<val_t>]; if (!def.type) { … }`. A
10/// `glz::meta::name` that is the same string for two instantiations therefore
11/// makes the second one silently `$ref` the first one's definition, and a
12/// renderer that resolves the `$ref` reads the wrong payload type.
13///
14/// This header is the single place the forms layer composes those keys, so the
15/// rule that keeps them apart is stated once rather than once per wrapper.
16///
17/// Two rules shape what goes into a key:
18///
19/// - **Nothing compiler-derived.** `glz::name_v` falls back to a
20/// `__PRETTY_FUNCTION__`/`__FUNCSIG__`-derived spelling that differs between
21/// compilers, and a `$defs` key that changed with the compiler would make a
22/// schema unreadable by a peer build — the same reason
23/// `core/payload_schema.hpp` refuses it for the payload fingerprint. Keys
24/// here are built only from template arguments spelled in these sources.
25/// - **Discriminate exactly what the definition's *content* depends on.** Two
26/// instantiations whose `$defs` entries would be byte-identical *should*
27/// share one entry; that is what `$defs` is for. Splitting them would only
28/// bloat the document. So a key carries the parameters the entry's shape
29/// varies with, and nothing else.
30
31#include <array>
32#include <concepts>
33#include <cstddef>
34#include <string_view>
35#include <type_traits>
36
38
39namespace morph::forms::detail {
40
41/// @brief Width one `FixedString` occupies in a composed key.
42///
43/// A `_` inside a part is written doubled (see `choiceSchemaNameStorage`), so a
44/// part is as wide as its text plus one extra character per `_` it contains.
45///
46/// @tparam N Storage size including the terminating null.
47/// @param text The compile-time string to measure.
48/// @return The number of characters @p text contributes to a key.
49template <std::size_t N>
50[[nodiscard]] consteval std::size_t escapedPartWidth(const ::morph::detail::FixedString<N>& text) noexcept {
51 std::size_t width = 0;
52 for (char const character : text.view()) {
53 width += (character == '_') ? 2U : 1U;
54 }
55 return width;
56}
57
58/// @brief Whether @p text begins or ends with `_`.
59///
60/// Such a part is rejected by `choiceSchemaNameStorage`; see the note on the
61/// escape's injectivity there.
62///
63/// @tparam N Storage size including the terminating null.
64/// @param text The compile-time string to inspect.
65/// @return `true` when the first or last character is `_`.
66template <std::size_t N>
67[[nodiscard]] consteval bool hasBoundaryUnderscore(const ::morph::detail::FixedString<N>& text) noexcept {
68 std::string_view const view = text.view();
69 return !view.empty() && (view.front() == '_' || view.back() == '_');
70}
71
72/// @brief Storage for a `Choice`'s `$defs` key.
73///
74/// `inline constexpr` gives it static storage duration, which is what lets a
75/// `std::string_view` into it serve as a `glz::meta::name`.
76///
77/// The key is composed from every `FixedString` template argument, joined by a
78/// single `_`. A `_` *inside* a part is written doubled, which is what makes
79/// the join injective: without that escape two different splits of the same
80/// characters alias (`ValueField = "id_x", LabelField = "name"` against
81/// `"id", "x_name"` — both plausible snake_case wire names — would produce one
82/// key, and the second `Choice` would then `$ref` the first one's definition).
83///
84/// Doubling makes every `_` run *inside* an escaped part even-length, so a run
85/// that contains a join is odd — which is what tells the two apart, and it
86/// holds **only while no part begins or ends with `_`**. Let one do, and the
87/// join becomes ambiguous again: `("id_", "name")` and `("id", "_name")` both
88/// spell `…_id___name`, because an escaped pair adjoining a join is
89/// indistinguishable from a join adjoining an escaped pair. The `static_assert`
90/// below rejects that spelling rather than let it alias silently; a wire field
91/// whose name has a leading or trailing `_` has to be renamed (or given a
92/// distinct `OptionsAction`) to be usable here.
93///
94/// @tparam OptionsAction Type id of the action serving the options.
95/// @tparam ValueField Result-row field submitted as the value.
96/// @tparam LabelField Result-row field shown to the user.
97/// @tparam DependsOn Wire names of the sibling fields parameterising the
98/// options action.
99template <::morph::detail::FixedString OptionsAction, ::morph::detail::FixedString ValueField,
100 ::morph::detail::FixedString LabelField, ::morph::detail::FixedString... DependsOn>
101inline constexpr auto choiceSchemaNameStorage = [] {
102 static_assert(!hasBoundaryUnderscore(OptionsAction) && !hasBoundaryUnderscore(ValueField) &&
103 !hasBoundaryUnderscore(LabelField) && !(hasBoundaryUnderscore(DependsOn) || ...),
104 "Choice: an options action or field name may not begin or end with '_' -- the schema key that "
105 "keeps two Choice instantiations apart cannot be composed unambiguously from such a name");
106
107 constexpr std::string_view prefix{"Choice_"};
108 constexpr std::size_t total = prefix.size() + escapedPartWidth(OptionsAction) + 1U + escapedPartWidth(ValueField) +
109 1U + escapedPartWidth(LabelField) +
110 ((1U + escapedPartWidth(DependsOn)) + ... + std::size_t{0});
111
112 std::array<char, total> out{};
113 std::size_t written = 0;
114 // Both lambdas index `out` with a running offset the size arithmetic above
115 // has already bounded, exactly as `detail::FixedString`'s own constructor
116 // does -- hence the same suppressions it carries.
117 auto append = [&out, &written](std::string_view part) {
118 for (char const character : part) {
119 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
120 out[written] = character;
121 ++written;
122 }
123 };
124 // Writes a template argument, doubling any `_` so the single-`_` joins
125 // above stay unambiguous.
126 auto appendPart = [&out, &written](std::string_view part) {
127 for (char const character : part) {
128 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
129 out[written] = character;
130 ++written;
131 if (character == '_') {
132 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
133 out[written] = character;
134 ++written;
135 }
136 }
137 };
138
139 append(prefix);
140 appendPart(OptionsAction.view());
141 append("_");
142 appendPart(ValueField.view());
143 append("_");
144 appendPart(LabelField.view());
145 ((append("_"), appendPart(DependsOn.view())), ...);
146 return out;
147}();
148
149/// @brief The `$defs` key for one `Choice` instantiation.
150///
151/// Distinct for every distinct set of `FixedString` arguments. Two `Choice`
152/// fields naming the *same* options action **and** the same value field still
153/// share a key: they read one column of one result set, which has one type, so
154/// a differing `T` between them is an author error rather than a shape this
155/// key has to keep apart.
156///
157/// @tparam OptionsAction Type id of the action serving the options.
158/// @tparam ValueField Result-row field submitted as the value.
159/// @tparam LabelField Result-row field shown to the user.
160/// @tparam DependsOn Wire names of the sibling fields parameterising the
161/// options action.
162template <::morph::detail::FixedString OptionsAction, ::morph::detail::FixedString ValueField,
163 ::morph::detail::FixedString LabelField, ::morph::detail::FixedString... DependsOn>
164inline constexpr std::string_view choiceSchemaName{
165 choiceSchemaNameStorage<OptionsAction, ValueField, LabelField, DependsOn...>.data(),
166 choiceSchemaNameStorage<OptionsAction, ValueField, LabelField, DependsOn...>.size()};
167
168/// @brief A short, fixed-capacity compile-time text buffer.
169///
170/// A literal type with public members, so it can be returned from a `consteval`
171/// function and read in a constant expression. Sized for the longest string
172/// built through it — `"Ranged_"` plus the widest tag `numericShapeTag`
173/// produces (`"bool"`, `"f128"`) — with room to spare.
174///
175/// `Choice` keys do not go through this: their length depends on template
176/// arguments of unbounded length, so `choiceSchemaNameStorage` sizes its array
177/// exactly instead.
178struct ShapeTag {
179 /// @brief Character storage; only the first `size` entries are meaningful.
180 std::array<char, 16> characters{};
181 /// @brief Number of meaningful characters in `characters`.
182 std::size_t size = 0;
183
184 /// @brief Appends @p text.
185 /// @param text The characters to append.
186 constexpr void append(std::string_view text) noexcept {
187 for (char const character : text) {
188 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
189 characters[size] = character;
190 ++size;
191 }
192 }
193
194 /// @brief Appends @p value's decimal digits.
195 /// @param value The number to render.
196 constexpr void appendDecimal(std::size_t value) noexcept {
197 std::array<char, 20> digits{};
198 std::size_t count = 0;
199 while (value != 0) {
200 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
201 digits[count] = static_cast<char>('0' + static_cast<char>(value % 10U));
202 ++count;
203 value /= 10U;
204 }
205 if (count == 0) {
206 digits.front() = '0';
207 count = 1;
208 }
209 while (count > 0) {
210 --count;
211 // NOLINTNEXTLINE(cppcoreguidelines-pro-bounds-constant-array-index, cppcoreguidelines-pro-bounds-avoid-unchecked-container-access)
212 characters[size] = digits[count];
213 ++size;
214 }
215 }
216
217 /// @brief A view of the meaningful characters.
218 /// @return The rendered tag.
219 [[nodiscard]] constexpr std::string_view view() const noexcept { return {characters.data(), size}; }
220};
221
222/// @brief Trait: is @p T one of the character types glaze schematises as a
223/// JSON string rather than as a number?
224///
225/// `char` is the one that matters in practice: glaze writes
226/// `{"type":"string"}` for it while `std::int8_t` (i.e. `signed char`) gets
227/// `{"type":"integer", …}`. Both are arithmetic and both are 8 bits wide, so a
228/// tag built from signedness and width alone would put them on one `$defs`
229/// entry describing only one of them. The sibling character types are listed
230/// with it because they are schematised the same way.
231///
232/// @tparam T Type to test.
233template <typename T>
234inline constexpr bool isCharacterType =
235 std::same_as<T, char> || std::same_as<T, char8_t> || std::same_as<T, char16_t> || std::same_as<T, char32_t> ||
236 std::same_as<T, wchar_t>;
237
238/// @brief A deterministic tag for the JSON-schema shape of an arithmetic type.
239///
240/// The shape a nullable arithmetic type generates is fixed by four things and
241/// no others: whether it is `bool`, whether it is a character type (which
242/// glaze writes as a JSON string), whether it is floating-point, whether it is
243/// signed, and how wide it is. So the tag carries exactly those — `"bool"`, or
244/// `c`/`f`/`i`/`u` followed by the width in bits. Two types that share a tag
245/// generate byte-identical definitions, which is precisely when sharing one
246/// `$defs` entry is correct rather than a collision.
247///
248/// Derived from `sizeof` and the standard type traits, never from a compiler's
249/// own spelling of the type.
250///
251/// @tparam T The arithmetic type.
252/// @return The tag.
253template <typename T>
254 requires std::is_arithmetic_v<T>
255[[nodiscard]] consteval ShapeTag numericShapeTag() {
256 ShapeTag tag{};
257 if constexpr (std::same_as<T, bool>) {
258 tag.append("bool");
259 } else {
260 if constexpr (isCharacterType<T>) {
261 tag.append("c");
262 } else if constexpr (std::floating_point<T>) {
263 tag.append("f");
264 } else if constexpr (std::is_signed_v<T>) {
265 tag.append("i");
266 } else {
267 tag.append("u");
268 }
269 tag.appendDecimal(sizeof(T) * 8U);
270 }
271 return tag;
272}
273
274/// @brief Storage for a `Ranged`'s `$defs` key.
275///
276/// `inline constexpr` gives it static storage duration, which is what lets the
277/// `std::string_view` below serve as a `glz::meta::name`.
278/// @tparam T The payload's arithmetic type (`decltype(Min)`).
279template <typename T>
280 requires std::is_arithmetic_v<T>
281inline constexpr auto rangedSchemaNameStorage = [] {
282 ShapeTag key{};
283 key.append("Ranged_");
284 key.append(numericShapeTag<T>().view());
285 return key;
286}();
287
288/// @brief The `$defs` key for one `Ranged` instantiation.
289///
290/// Keyed on the payload type alone, deliberately: a `Ranged`'s definition is
291/// the schema of `std::optional<decltype(Min)>` and nothing more — the bounds
292/// themselves are emitted as property-level `x-min`/`x-max`/`x-step`, never
293/// into the `$def`. So `Ranged<0, 100>` and `Ranged<5, 50>` share one entry
294/// because their entries *are* the same entry, while `Ranged<0.0, 1.0, 0.1>`
295/// gets its own.
296///
297/// @tparam T The payload's arithmetic type (`decltype(Min)`).
298template <typename T>
299 requires std::is_arithmetic_v<T>
300inline constexpr std::string_view rangedSchemaName = rangedSchemaNameStorage<T>.view();
301
302} // namespace morph::forms::detail
Node text(TextOptions const &options)
A text node.
Definition author.hpp:3126
Handle< std::int64_t > count(AnyHandle const &list)
count: a list's length.
Definition author.hpp:1000
void append(Json &json, char const *key, Json item)
Appends item to the array at key of json, making the array on first use.
Definition author.hpp:2481
Shared compile-time, NTTP-capable fixed string.
constexpr std::string_view view() const noexcept
A view of the string (without the terminating null).
std::array< char, N > data
Character storage (null-terminated).