morph 0.1.0
Asynchronous bridge between GUI thread and business models
Loading...
Searching...
No Matches
value.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: Apache-2.0
2
3#pragma once
4#include <algorithm>
5#include <array>
6#include <compare>
7#include <concepts>
8#include <cstddef>
9#include <cstdint>
10#include <expected>
11#include <functional>
12#include <glaze/glaze.hpp>
13#include <limits>
14#include <map>
15#include <memory>
16#include <string>
17#include <string_view>
18#include <utility>
19#include <variant>
20#include <vector>
21
22#include "../util/datetime.hpp"
23#include "../util/rational.hpp"
24
25/// @file
26/// @brief The UI document's typed values (`ui::Value`), its types (`ui::Type`), their JSON form, and `ui::Refusal`,
27/// the error every load-time check of the document layer returns.
28///
29/// Specified in `docs/spec/ui/document.md`, "Values and types".
30
31namespace morph::ui {
32
33/// @brief The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mode,
34/// so an integer is never read through a double.
35using Json = glz::generic_u64;
36
37/// @brief Why a document, an expression or a value was refused: where, and the reason.
38struct Refusal {
39 /// @brief The id of the document refused; empty when the refusal is not about a whole document.
40 std::string document;
41 /// @brief A JSON Pointer (RFC 6901) to the refused part, `""` for the whole input.
42 std::string path;
43 /// @brief The reason, in one sentence.
44 std::string reason;
45
46 /// @brief The refusal as one line of text.
47 /// @return `document <id> at <path>: <reason>`, leaving out the parts that are empty.
48 [[nodiscard]] std::string message() const {
49 std::string out;
50 if (!document.empty()) {
51 out += "document " + document + " ";
52 }
53 out += "at " + (path.empty() ? std::string{"/"} : path) + ": " + reason;
54 return out;
55 }
56
57 /// @brief Field-wise equality.
58 /// @param other The refusal to compare with.
59 /// @return Whether the document, path and reason are equal.
60 bool operator==(Refusal const& other) const = default;
61};
62
63/// @brief Appends one reference token to a JSON Pointer, escaping `~` and `/` as RFC 6901 requires.
64/// @param base The pointer to extend, `""` for the root.
65/// @param token The object key to append.
66/// @return `base + "/" + escaped(token)`.
67// NOLINTNEXTLINE(bugprone-easily-swappable-parameters): the pointer, then the token appended to it, as they read.
68[[nodiscard]] inline std::string pointerAppend(std::string_view base, std::string_view token) {
69 std::string out{base};
70 out += '/';
71 for (char const chr : token) {
72 if (chr == '~') {
73 out += "~0";
74 } else if (chr == '/') {
75 out += "~1";
76 } else {
77 out += chr;
78 }
79 }
80 return out;
81}
82
83/// @brief Appends an array index to a JSON Pointer.
84/// @param base The pointer to extend, `""` for the root.
85/// @param index The array index to append.
86/// @return `base + "/" + index`.
87[[nodiscard]] inline std::string pointerAppend(std::string_view base, std::size_t index) {
88 return std::string{base} + "/" + std::to_string(index);
89}
90
91/// @brief An exact decimal: a `math::Rational` whose `DecimalPlaces` are the value's declared places.
92///
93/// The places are part of the value: two decimals equal in magnitude and different in places compare unequal,
94/// because they display differently. `expr/1`'s `eq` compares magnitudes.
95struct Decimal {
96 /// @brief The exact value, with its declared places.
98
99 /// @brief Equality of magnitude and places.
100 /// @param other The decimal to compare with.
101 /// @return Whether both the canonical fraction and the places are equal.
102 [[nodiscard]] bool operator==(Decimal const& other) const noexcept {
103 return value.numerator == other.value.numerator && value.denominator == other.value.denominator &&
104 value.decimalPlaces == other.value.decimalPlaces;
105 }
106};
107
108/// @brief An exact decimal with a unit: the id of a unit of the server's unit systems (`x-unitAlternatives`).
109struct Quantity {
110 /// @brief The exact magnitude, with its declared places.
112 /// @brief The unit id, such as `mg/L`.
113 std::string unit;
114
115 /// @brief Equality of magnitude, places and unit.
116 /// @param other The quantity to compare with.
117 /// @return Whether magnitude, places and unit are all equal.
118 [[nodiscard]] bool operator==(Quantity const& other) const noexcept {
119 return Decimal{value} == Decimal{other.value} && unit == other.unit;
120 }
121};
122
123class Value;
124
125/// @brief A list value.
126using ValueList = std::vector<Value>;
127
128/// @brief A record value: field name to value, ordered by name.
129using ValueRecord = std::map<std::string, Value, std::less<>>;
130
131/// @brief The kinds a `Value` holds.
132enum class ValueKind : std::uint8_t {
133 Null, ///< No value: absent, not entered, or the result of an error.
134 Bool, ///< `bool`.
135 Int, ///< `int`: a 64-bit signed integer, exact.
136 String, ///< `string`: UTF-8 text.
137 Decimal, ///< `decimal`: an exact rational with declared places.
138 Quantity, ///< `quantity`: a decimal with a unit.
139 Timestamp, ///< `timestamp`: a UTC instant with millisecond precision.
140 List, ///< `list<T>`.
141 Record, ///< A record of named fields.
142};
143
144/// @brief The name of a value kind, as a document spells its type.
145/// @param kind The kind.
146/// @return `null`, `bool`, `int`, `string`, `decimal`, `quantity`, `timestamp`, `list` or `record`.
147[[nodiscard]] constexpr std::string_view kindName(ValueKind kind) {
148 constexpr std::array<std::string_view, 9> kNames = {"null", "bool", "int", "string", "decimal",
149 "quantity", "timestamp", "list", "record"};
150 return kNames.at(static_cast<std::size_t>(kind));
151}
152
153/// @brief The document's typed value (spec 5 §3): what every expression evaluates to.
154///
155/// A value is immutable once built. Lists and records share their storage between copies, so copying a value —
156/// which the reactive core does on every read — costs a reference count, not a deep copy. Equality is deep.
157class Value {
158public:
159 /// @brief Constructs `null`.
160 Value() noexcept = default;
161
162 /// @brief Constructs `null`.
163 /// @param null Unused.
164 Value(std::nullptr_t null) noexcept { static_cast<void>(null); } // NOLINT(google-explicit-constructor)
165
166 /// @brief Constructs a `bool`.
167 /// @param flag The value.
168 Value(bool flag) noexcept : _data{flag} {} // NOLINT(google-explicit-constructor)
169
170 /// @brief Constructs an `int` from any signed integer.
171 /// @tparam I A signed integer type.
172 /// @param number The value.
173 template <std::signed_integral I>
174 Value(I number) noexcept : _data{static_cast<std::int64_t>(number)} {} // NOLINT(google-explicit-constructor)
175
176 /// @brief Constructs a `string`.
177 /// @param text UTF-8 text.
178 Value(std::string text) noexcept : _data{std::move(text)} {} // NOLINT(google-explicit-constructor)
179
180 /// @brief Constructs a `string` from a C string, which would otherwise convert to `bool`.
181 /// @param text NUL-terminated UTF-8 text.
182 Value(char const* text) : _data{std::string{text}} {} // NOLINT(google-explicit-constructor)
183
184 /// @brief Constructs a `decimal`.
185 /// @param number The exact value with its places.
186 Value(Decimal number) noexcept : _data{number} {} // NOLINT(google-explicit-constructor)
187
188 /// @brief Constructs a `quantity`.
189 /// @param quantity The magnitude and unit.
190 Value(Quantity quantity) noexcept : _data{std::move(quantity)} {} // NOLINT(google-explicit-constructor)
191
192 /// @brief Constructs a `timestamp`.
193 /// @param instant The UTC instant.
194 Value(time::DateTime instant) noexcept : _data{instant} {} // NOLINT(google-explicit-constructor)
195
196 /// @brief Constructs a list.
197 /// @param items The elements.
198 Value(ValueList items) // NOLINT(google-explicit-constructor)
199 : _data{std::make_shared<ValueList const>(std::move(items))} {}
200
201 /// @brief Constructs a record.
202 /// @param fields The fields.
203 Value(ValueRecord fields) // NOLINT(google-explicit-constructor)
204 : _data{std::make_shared<ValueRecord const>(std::move(fields))} {}
205
206 /// @brief The kind of value held.
207 /// @return The kind.
208 [[nodiscard]] ValueKind kind() const noexcept { return static_cast<ValueKind>(_data.index()); }
209
210 /// @brief Whether the value is `null`.
211 /// @return `kind() == ValueKind::Null`.
212 [[nodiscard]] bool isNull() const noexcept { return _data.index() == 0; }
213
214 /// @brief The `bool`, if held.
215 /// @return A pointer to it, or `nullptr`.
216 [[nodiscard]] bool const* asBool() const noexcept { return std::get_if<bool>(&_data); }
217
218 /// @brief The `int`, if held.
219 /// @return A pointer to it, or `nullptr`.
220 [[nodiscard]] std::int64_t const* asInt() const noexcept { return std::get_if<std::int64_t>(&_data); }
221
222 /// @brief The `string`, if held.
223 /// @return A pointer to it, or `nullptr`.
224 [[nodiscard]] std::string const* asString() const noexcept { return std::get_if<std::string>(&_data); }
225
226 /// @brief The `decimal`, if held.
227 /// @return A pointer to it, or `nullptr`.
228 [[nodiscard]] Decimal const* asDecimal() const noexcept { return std::get_if<Decimal>(&_data); }
229
230 /// @brief The `quantity`, if held.
231 /// @return A pointer to it, or `nullptr`.
232 [[nodiscard]] Quantity const* asQuantity() const noexcept { return std::get_if<Quantity>(&_data); }
233
234 /// @brief The `timestamp`, if held.
235 /// @return A pointer to it, or `nullptr`.
236 [[nodiscard]] time::DateTime const* asTimestamp() const noexcept { return std::get_if<time::DateTime>(&_data); }
237
238 /// @brief The list, if held.
239 /// @return A pointer to it, or `nullptr`.
240 [[nodiscard]] ValueList const* asList() const noexcept {
241 auto const* held = std::get_if<std::shared_ptr<ValueList const>>(&_data);
242 return held == nullptr ? nullptr : held->get();
243 }
244
245 /// @brief The record, if held.
246 /// @return A pointer to it, or `nullptr`.
247 [[nodiscard]] ValueRecord const* asRecord() const noexcept {
248 auto const* held = std::get_if<std::shared_ptr<ValueRecord const>>(&_data);
249 return held == nullptr ? nullptr : held->get();
250 }
251
252 /// @brief A field of a record.
253 /// @param name The field name.
254 /// @return A pointer to the field, or `nullptr` when this is not a record or has no such field.
255 [[nodiscard]] Value const* field(std::string_view name) const {
256 ValueRecord const* const record = asRecord();
257 if (record == nullptr) {
258 return nullptr;
259 }
260 auto const found = record->find(name);
261 return found == record->end() ? nullptr : &found->second;
262 }
263
264 /// @brief Deep equality: same kind and equal content; a decimal's places take part.
265 /// @param other The value to compare with.
266 /// @return Whether the two values are equal.
267 // NOLINTNEXTLINE(misc-no-recursion): a list or a record compares its elements, which are values.
268 [[nodiscard]] bool operator==(Value const& other) const {
269 if (_data.index() != other._data.index()) {
270 return false;
271 }
272 if (ValueList const* const mine = asList()) {
273 return *mine == *other.asList();
274 }
275 if (ValueRecord const* const mine = asRecord()) {
276 return *mine == *other.asRecord();
277 }
278 return _data == other._data;
279 }
280
281private:
282 std::variant<std::monostate, bool, std::int64_t, std::string, Decimal, Quantity, time::DateTime,
283 std::shared_ptr<ValueList const>, std::shared_ptr<ValueRecord const>>
284 _data;
285};
286
287// GCC's -Wshadow reports `Key` as shadowing `morph::ui::Key` (view.hpp) whenever that alias is
288// declared first. A scoped enumerator is only ever named as `TypeKind::Key`, so nothing is shadowed.
289#if defined(__GNUC__) && !defined(__clang__)
290#pragma GCC diagnostic push
291#pragma GCC diagnostic ignored "-Wshadow"
292#endif
293/// @brief The kinds of a `Type`.
294enum class TypeKind : std::uint8_t {
295 Any, ///< Not known to the document: a query result, a reply, an element's value.
296 Bool, ///< `bool`.
297 Int, ///< `int`.
298 String, ///< `string`.
299 Decimal, ///< `decimal`.
300 Quantity, ///< `quantity`.
301 Timestamp, ///< `timestamp`.
302 Key, ///< `key`: an `int` or a `string`.
303 Record, ///< `record`: a record whose fields the document does not declare.
304 List, ///< `list<T>`.
305};
306#if defined(__GNUC__) && !defined(__clang__)
307#pragma GCC diagnostic pop
308#endif
309
310/// @brief A document type (spec 5 §3): a kind, whether `null` is allowed, and a list's element type.
311struct Type {
312 /// @brief The kind.
314 /// @brief Whether the type is `T?`, admitting `null`.
315 bool optional = false;
316 /// @brief The element type of a `list<T>`; `nullptr` for any other kind.
317 std::shared_ptr<Type const> element;
318
319 /// @brief The type the document cannot know.
320 /// @return `any`.
321 [[nodiscard]] static Type any() { return Type{}; }
322
323 /// @brief A scalar or record type.
324 /// @param kind The kind; not `List`.
325 /// @param optional Whether it admits `null`.
326 /// @return The type.
327 [[nodiscard]] static Type of(TypeKind kind, bool optional = false) {
328 return Type{.kind = kind, .optional = optional, .element = nullptr};
329 }
330
331 /// @brief A list type.
332 /// @param element The element type.
333 /// @param optional Whether the list itself admits `null`.
334 /// @return `list<element>`, or `list<element>?`.
335 [[nodiscard]] static Type listOf(Type element, bool optional = false) {
336 return Type{
337 .kind = TypeKind::List, .optional = optional, .element = std::make_shared<Type const>(std::move(element))};
338 }
339
340 /// @brief Whether the type is `any`.
341 /// @return `kind == TypeKind::Any`.
342 [[nodiscard]] bool isAny() const noexcept { return kind == TypeKind::Any; }
343
344 /// @brief The type as a document spells it.
345 /// @return For example `int`, `decimal?`, `list<key>`.
346 // NOLINTNEXTLINE(misc-no-recursion): a list type names its element type.
347 [[nodiscard]] std::string name() const {
348 constexpr std::array<std::string_view, 10> kNames = {"any", "bool", "int", "string", "decimal",
349 "quantity", "timestamp", "key", "record", "list"};
350 std::string out{kNames.at(static_cast<std::size_t>(kind))};
351 if (kind == TypeKind::List) {
352 out += "<" + (element ? element->name() : std::string{"any"}) + ">";
353 }
354 if (optional) {
355 out += '?';
356 }
357 return out;
358 }
359
360 /// @brief Whether a value conforms to the type.
361 ///
362 /// `null` conforms to an optional type and to `any`; an `int` conforms to `decimal`, which holds it exactly; a
363 /// list conforms when every element conforms to the element type.
364 /// @param value The value to test.
365 /// @return Whether @p value is a value of this type.
366 // NOLINTNEXTLINE(misc-no-recursion): a list type admits a list whose elements its element type admits.
367 [[nodiscard]] bool admits(Value const& value) const {
368 if (kind == TypeKind::Any) {
369 return true;
370 }
371 if (value.isNull()) {
372 return optional;
373 }
374 ValueKind const held = value.kind();
375 if (kind == TypeKind::Key) {
376 return held == ValueKind::Int || held == ValueKind::String;
377 }
378 if (kind == TypeKind::Decimal && held == ValueKind::Int) {
379 return true;
380 }
381 if (kind != TypeKind::List) {
382 // The value kind each scalar or record type holds, indexed by `TypeKind`; `any` and `key` are handled
383 // above, so their entries are never read.
384 constexpr std::array<ValueKind, 10> kHolds = {
387 return kHolds.at(static_cast<std::size_t>(kind)) == held;
388 }
389 ValueList const* const items = value.asList();
390 if (items == nullptr) {
391 return false;
392 }
393 // NOLINTNEXTLINE(misc-no-recursion): each element is admitted by the element type.
394 return std::ranges::all_of(*items, [this](Value const& item) { return !element || element->admits(item); });
395 }
396
397 /// @brief Deep equality.
398 /// @param other The type to compare with.
399 /// @return Whether kind, optionality and element type are equal.
400 // NOLINTNEXTLINE(misc-no-recursion): a list type compares its element types.
401 [[nodiscard]] bool operator==(Type const& other) const {
402 if (kind != other.kind || optional != other.optional) {
403 return false;
404 }
405 if (!element || !other.element) {
406 return !element && !other.element;
407 }
408 return *element == *other.element;
409 }
410};
411
412namespace detail {
413
414/// @brief Parses one type at the front of @p text, advancing it.
415/// @param text The remaining text; on success, what follows the type.
416/// @param depth The `list<>` nesting depth so far, bounded so a hostile type cannot recurse without limit.
417/// @return The type, or the reason it does not parse.
418// NOLINTNEXTLINE(misc-no-recursion): `list<T>` parses `T`; the depth bound stops a hostile nesting.
419[[nodiscard]] inline std::expected<Type, std::string> parseTypePrefix(std::string_view& text, int depth) {
420 constexpr int kMaxDepth = 8;
421 if (depth > kMaxDepth) {
422 return std::unexpected(std::string{"type nests list<> too deeply"});
423 }
424 static constexpr std::array<std::pair<std::string_view, TypeKind>, 9> kScalars = {{
425 {"bool", TypeKind::Bool},
426 {"int", TypeKind::Int},
427 {"string", TypeKind::String},
428 {"decimal", TypeKind::Decimal},
429 {"quantity", TypeKind::Quantity},
430 {"timestamp", TypeKind::Timestamp},
431 {"key", TypeKind::Key},
432 {"record", TypeKind::Record},
433 {"any", TypeKind::Any},
434 }};
435 Type result;
436 if (text.starts_with("list<")) {
437 text.remove_prefix(5);
438 auto inner = parseTypePrefix(text, depth + 1);
439 if (!inner) {
440 return inner;
441 }
442 if (!text.starts_with('>')) {
443 return std::unexpected(std::string{"list<...> is not closed with >"});
444 }
445 text.remove_prefix(1);
446 result = Type::listOf(std::move(*inner));
447 } else {
448 std::size_t length = 0;
449 while (length < text.size() && text.at(length) >= 'a' && text.at(length) <= 'z') {
450 ++length;
451 }
452 std::string_view const word = text.substr(0, length);
453 bool known = false;
454 for (auto const& [spelling, kind] : kScalars) {
455 if (spelling == word) {
456 result = Type::of(kind);
457 known = true;
458 break;
459 }
460 }
461 if (!known) {
462 return std::unexpected("unknown type `" + std::string{word} + "`");
463 }
464 text.remove_prefix(length);
465 }
466 if (text.starts_with('?')) {
467 text.remove_prefix(1);
468 result.optional = result.kind != TypeKind::Any;
469 }
470 return result;
471}
472
473} // namespace detail
474
475/// @brief Parses a document type: `bool`, `int`, `string`, `decimal`, `quantity`, `timestamp`, `key`, `record`,
476/// `any`, `list<T>`, each optionally followed by `?`.
477/// @param text The type as written.
478/// @return The type, or the reason it does not parse.
479[[nodiscard]] inline std::expected<Type, std::string> parseType(std::string_view text) {
480 std::string_view rest = text;
481 auto parsed = detail::parseTypePrefix(rest, 0);
482 if (!parsed) {
483 return parsed;
484 }
485 if (!rest.empty()) {
486 return std::unexpected("unexpected `" + std::string{rest} + "` after the type");
487 }
488 return parsed;
489}
490
491namespace detail {
492
493/// @brief Reads a JSON integer as an int64.
494/// @param json The JSON value.
495/// @return The integer; `std::nullopt` when it is not an integer or does not fit int64.
496[[nodiscard]] inline std::optional<std::int64_t> jsonInt(Json const& json) noexcept {
497 if (auto const* const signedValue = std::get_if<std::int64_t>(&json.data)) {
498 return *signedValue;
499 }
500 if (auto const* const unsignedValue = std::get_if<std::uint64_t>(&json.data)) {
501 if (*unsignedValue <= static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
502 return static_cast<std::int64_t>(*unsignedValue);
503 }
504 }
505 return std::nullopt;
506}
507
508/// @brief Reads the wire form of a decimal or quantity: an object of exactly `num`, `den`, `dp` (and `unit`).
509/// @param object The JSON object.
510/// @return The value when @p object has that shape; `std::nullopt` when it is some other record; the reason when it
511/// has the shape and a bad member.
512[[nodiscard]] inline std::optional<std::expected<Value, std::string>> exactFromJson(Json::object_t const& object) {
513 if (object.size() != 3 && object.size() != 4) {
514 return std::nullopt;
515 }
516 Json const* num = nullptr;
517 Json const* den = nullptr;
518 Json const* places = nullptr;
519 Json const* unit = nullptr;
520 for (auto const& [name, member] : object) {
521 if (name == "num") {
522 num = &member;
523 } else if (name == "den") {
524 den = &member;
525 } else if (name == "dp") {
526 places = &member;
527 } else if (name == "unit") {
528 unit = &member;
529 } else {
530 return std::nullopt;
531 }
532 }
533 if (num == nullptr || den == nullptr || places == nullptr) {
534 return std::nullopt;
535 }
536 auto const numerator = jsonInt(*num);
537 auto const denominator = jsonInt(*den);
538 auto const decimals = jsonInt(*places);
539 if (!numerator || !denominator || !decimals) {
540 return std::unexpected(std::string{"num, den and dp of an exact value are integers"});
541 }
542 if (*decimals < 0 || std::cmp_greater(*decimals, math::kMaxDecimalPlaces)) {
543 return std::unexpected(std::string{"dp of an exact value is between 0 and 18"});
544 }
545 auto rational = math::Rational::from(math::Numerator{*numerator}, math::Denominator{*denominator},
546 math::DecimalPlaces{static_cast<std::uint32_t>(*decimals)});
547 if (!rational) {
548 return std::unexpected(std::string{"den of an exact value is not zero"});
549 }
550 if (unit == nullptr) {
551 return Value{Decimal{*rational}};
552 }
553 auto const* const unitText = std::get_if<std::string>(&unit->data);
554 if (unitText == nullptr) {
555 return std::unexpected(std::string{"unit of a quantity is a string"});
556 }
557 return Value{Quantity{.value = *rational, .unit = *unitText}};
558}
559
560} // namespace detail
561
562/// @brief Reads a value from its JSON form, structurally.
563///
564/// `null`, booleans, integers, strings, arrays and objects map to their kinds. An object of exactly `num`, `den` and
565/// `dp` is a decimal, and with `unit` a quantity: the wire form of `math::Rational`. A number with a fraction or an
566/// exponent is refused, because an exact value is never written as a double; an integer beyond int64 is refused.
567/// A timestamp has no structural form — it is a string — so it is read through the typed overload.
568/// @param json The JSON value.
569/// @return The value, or the reason it is refused.
570[[nodiscard]] inline std::expected<Value, std::string> valueFromJson(Json const& json) {
571 struct Visitor {
572 std::expected<Value, std::string> operator()(std::nullptr_t) const { return Value{}; }
573 std::expected<Value, std::string> operator()(bool flag) const { return Value{flag}; }
574 std::expected<Value, std::string> operator()(std::uint64_t number) const {
575 if (number > static_cast<std::uint64_t>(std::numeric_limits<std::int64_t>::max())) {
576 return std::unexpected(std::string{"integer does not fit int64"});
577 }
578 return Value{static_cast<std::int64_t>(number)};
579 }
580 std::expected<Value, std::string> operator()(std::int64_t number) const { return Value{number}; }
581 std::expected<Value, std::string> operator()(double number) const {
582 static_cast<void>(number);
583 return std::unexpected(
584 std::string{R"(a number with a fraction is not exact; write a decimal as {"num", "den", "dp"})"});
585 }
586 std::expected<Value, std::string> operator()(std::string const& text) const { return Value{text}; }
587 std::expected<Value, std::string> operator()(Json::array_t const& array) const {
588 ValueList items;
589 items.reserve(array.size());
590 for (Json const& element : array) {
591 auto item = valueFromJson(element);
592 if (!item) {
593 return item;
594 }
595 items.push_back(std::move(*item));
596 }
597 return Value{std::move(items)};
598 }
599 std::expected<Value, std::string> operator()(Json::object_t const& object) const {
600 if (auto exact = detail::exactFromJson(object)) {
601 return std::move(*exact);
602 }
603 ValueRecord fields;
604 for (auto const& [name, member] : object) {
605 auto field = valueFromJson(member);
606 if (!field) {
607 return field;
608 }
609 fields.insert_or_assign(name, std::move(*field));
610 }
611 return Value{std::move(fields)};
612 }
613 };
614 return std::visit(Visitor{}, json.data);
615}
616
617/// @brief Reads a value from its JSON form as a value of @p type.
618///
619/// A `timestamp` is read from its ISO-8601 text; an `int` given for a `decimal` becomes a decimal of zero places; a
620/// list's elements are read as its element type. The result must then conform to @p type.
621/// @param json The JSON value.
622/// @param type The type the value must have.
623/// @return The value, or the reason it is refused.
624// NOLINTNEXTLINE(misc-no-recursion): a list is read element by element, as its element type.
625[[nodiscard]] inline std::expected<Value, std::string> valueFromJson(Json const& json, Type const& type) {
626 if (type.kind == TypeKind::Timestamp) {
627 if (auto const* const text = std::get_if<std::string>(&json.data)) {
628 auto const instant = time::DateTime::fromIso8601(*text);
629 if (!instant) {
630 return std::unexpected("`" + *text + "` is not an ISO-8601 UTC timestamp");
631 }
632 return Value{*instant};
633 }
634 }
635 if (type.kind == TypeKind::List && type.element) {
636 if (auto const* const array = std::get_if<Json::array_t>(&json.data)) {
637 ValueList items;
638 items.reserve(array->size());
639 for (Json const& element : *array) {
640 auto item = valueFromJson(element, *type.element);
641 if (!item) {
642 return item;
643 }
644 items.push_back(std::move(*item));
645 }
646 return Value{std::move(items)};
647 }
648 }
649 auto value = valueFromJson(json);
650 if (!value) {
651 return value;
652 }
653 if (type.kind == TypeKind::Decimal) {
654 if (std::int64_t const* const whole = value->asInt()) {
655 return Value{Decimal{math::Rational{*whole, math::DecimalPlaces{0}}}};
656 }
657 }
658 if (!type.admits(*value)) {
659 return std::unexpected("expected " + type.name() + ", got " + std::string{kindName(value->kind())});
660 }
661 return value;
662}
663
664/// @brief Writes a value in its JSON form: the inverse of `valueFromJson`, with a timestamp as ISO-8601 text.
665/// @param value The value.
666/// @return The JSON value.
667// NOLINTNEXTLINE(misc-no-recursion): a list or a record is written element by element.
668[[nodiscard]] inline Json valueToJson(Value const& value) {
669 Json out;
670 auto exact = [](math::Rational const& rational) {
671 Json::object_t object;
672 object.insert({"num", Json(rational.numerator)});
673 object.insert({"den", Json(rational.denominator)});
674 object.insert({"dp", Json(static_cast<std::int64_t>(rational.decimalPlaces.value))});
675 return object;
676 };
677 if (bool const* const flag = value.asBool()) {
678 out.data = *flag;
679 } else if (std::int64_t const* const number = value.asInt()) {
680 out.data = *number;
681 } else if (std::string const* const text = value.asString()) {
682 out.data = *text;
683 } else if (Decimal const* const decimal = value.asDecimal()) {
684 out.data = exact(decimal->value);
685 } else if (Quantity const* const quantity = value.asQuantity()) {
686 auto object = exact(quantity->value);
687 object.insert({"unit", Json(quantity->unit)});
688 out.data = std::move(object);
689 } else if (time::DateTime const* const instant = value.asTimestamp()) {
690 out.data = instant->toIso8601();
691 } else if (ValueList const* const items = value.asList()) {
692 Json::array_t array;
693 for (Value const& item : *items) {
694 array.push_back(valueToJson(item));
695 }
696 out.data = std::move(array);
697 } else if (ValueRecord const* const fields = value.asRecord()) {
698 Json::object_t object;
699 for (auto const& [name, field] : *fields) {
700 object.insert({name, valueToJson(field)});
701 }
702 out.data = std::move(object);
703 } else {
704 out.data = nullptr;
705 }
706 return out;
707}
708
709/// @brief The deepest a document's JSON may nest, counting each object and array as one level.
710///
711/// Documents written by hand or by the builders nest a dozen levels or so. The bound is checked before the JSON
712/// parser runs, because the parser and every later pass over the tree descend recursively, and a hostile document
713/// nested a few hundred levels deep overflows a 1 MB thread stack before anything else can refuse it.
714inline constexpr std::size_t kMaxJsonDepth = 64;
715
716namespace detail {
717
718/// @brief Whether JSON text nests objects and arrays deeper than `kMaxJsonDepth`.
719/// @param text The JSON text; brackets inside string literals do not count.
720/// @return True when some point of the text is inside more than `kMaxJsonDepth` objects and arrays.
721[[nodiscard]] inline bool nestsTooDeep(std::string_view text) {
722 std::size_t depth = 0;
723 bool inString = false;
724 bool escaped = false;
725 for (char const character : text) {
726 if (inString) {
727 if (escaped) {
728 escaped = false;
729 } else if (character == '\\') {
730 escaped = true;
731 } else if (character == '"') {
732 inString = false;
733 }
734 } else if (character == '"') {
735 inString = true;
736 } else if (character == '{' || character == '[') {
737 if (++depth > kMaxJsonDepth) {
738 return true;
739 }
740 } else if ((character == '}' || character == ']') && depth > 0) {
741 --depth;
742 }
743 }
744 return false;
745}
746
747} // namespace detail
748
749/// @brief Parses JSON text into the document layer's JSON tree.
750/// @param text The JSON text.
751/// @return The tree, or the reason the text is not JSON or nests deeper than `kMaxJsonDepth`.
752[[nodiscard]] inline std::expected<Json, std::string> parseJson(std::string_view text) {
754 return std::unexpected("the JSON nests deeper than " + std::to_string(kMaxJsonDepth) + " levels");
755 }
756 Json json;
757 if (auto const error = glz::read_json(json, text)) {
758 return std::unexpected("not JSON: " + glz::format_error(error, text));
759 }
760 return json;
761}
762
763} // namespace morph::ui
The document's typed value (spec 5 §3): what every expression evaluates to.
Definition value.hpp:157
Value(Decimal number) noexcept
Constructs a decimal.
Definition value.hpp:186
Value(time::DateTime instant) noexcept
Constructs a timestamp.
Definition value.hpp:194
bool operator==(Value const &other) const
Deep equality: same kind and equal content; a decimal's places take part.
Definition value.hpp:268
Quantity const * asQuantity() const noexcept
The quantity, if held.
Definition value.hpp:232
Value() noexcept=default
Constructs null.
Decimal const * asDecimal() const noexcept
The decimal, if held.
Definition value.hpp:228
Value(I number) noexcept
Constructs an int from any signed integer.
Definition value.hpp:174
Value(bool flag) noexcept
Constructs a bool.
Definition value.hpp:168
bool isNull() const noexcept
Whether the value is null.
Definition value.hpp:212
Value(ValueRecord fields)
Constructs a record.
Definition value.hpp:203
Value(Quantity quantity) noexcept
Constructs a quantity.
Definition value.hpp:190
Value(char const *text)
Constructs a string from a C string, which would otherwise convert to bool.
Definition value.hpp:182
Value const * field(std::string_view name) const
A field of a record.
Definition value.hpp:255
Value(ValueList items)
Constructs a list.
Definition value.hpp:198
ValueRecord const * asRecord() const noexcept
The record, if held.
Definition value.hpp:247
time::DateTime const * asTimestamp() const noexcept
The timestamp, if held.
Definition value.hpp:236
ValueKind kind() const noexcept
The kind of value held.
Definition value.hpp:208
bool const * asBool() const noexcept
The bool, if held.
Definition value.hpp:216
std::int64_t const * asInt() const noexcept
The int, if held.
Definition value.hpp:220
Value(std::string text) noexcept
Constructs a string.
Definition value.hpp:178
std::string const * asString() const noexcept
The string, if held.
Definition value.hpp:224
ValueList const * asList() const noexcept
The list, if held.
Definition value.hpp:240
UTC timestamps for morph actions.
@ List
{"list": [...]}.
Exact rational arithmetic for morph model/action values.
constexpr std::uint32_t kMaxDecimalPlaces
Largest decimal precision the type supports: 10^18 is the greatest power of ten that fits in std::int...
Definition rational.hpp:167
Strong type for a decimal-precision count.
Definition rational.hpp:121
Strong type for a Rational denominator.
Definition rational.hpp:154
Strong type for a Rational numerator.
Definition rational.hpp:140
Exact rational number with a runtime decimal-precision tag.
Definition rational.hpp:375
static constexpr std::expected< Rational, RationalError > from(Numerator wantedNumerator, Denominator wantedDenominator, DecimalPlaces wantedPrecision) noexcept
Validating factory.
Definition rational.hpp:416
A UTC instant with millisecond precision.
Definition datetime.hpp:71
static std::optional< DateTime > fromIso8601(std::string_view text) noexcept
Strictly parses ISO-8601 UTC: YYYY-MM-DDTHH:MM:SS, optionally followed by .m/.mm/....
Definition datetime.hpp:148
An exact decimal: a math::Rational whose DecimalPlaces are the value's declared places.
Definition value.hpp:95
math::Rational value
The exact value, with its declared places.
Definition value.hpp:97
bool operator==(Decimal const &other) const noexcept
Equality of magnitude and places.
Definition value.hpp:102
An exact decimal with a unit: the id of a unit of the server's unit systems (x-unitAlternatives).
Definition value.hpp:109
bool operator==(Quantity const &other) const noexcept
Equality of magnitude, places and unit.
Definition value.hpp:118
math::Rational value
The exact magnitude, with its declared places.
Definition value.hpp:111
std::string unit
The unit id, such as mg/L.
Definition value.hpp:113
Why a document, an expression or a value was refused: where, and the reason.
Definition value.hpp:38
bool operator==(Refusal const &other) const =default
Field-wise equality.
std::string document
The id of the document refused; empty when the refusal is not about a whole document.
Definition value.hpp:40
std::string path
A JSON Pointer (RFC 6901) to the refused part, "" for the whole input.
Definition value.hpp:42
std::string reason
The reason, in one sentence.
Definition value.hpp:44
std::string message() const
The refusal as one line of text.
Definition value.hpp:48
A document type (spec 5 §3): a kind, whether null is allowed, and a list's element type.
Definition value.hpp:311
std::string name() const
The type as a document spells it.
Definition value.hpp:347
bool optional
Whether the type is T?, admitting null.
Definition value.hpp:315
static Type any()
The type the document cannot know.
Definition value.hpp:321
static Type of(TypeKind kind, bool optional=false)
A scalar or record type.
Definition value.hpp:327
std::shared_ptr< Type const > element
The element type of a list<T>; nullptr for any other kind.
Definition value.hpp:317
TypeKind kind
The kind.
Definition value.hpp:313
bool operator==(Type const &other) const
Deep equality.
Definition value.hpp:401
bool admits(Value const &value) const
Whether a value conforms to the type.
Definition value.hpp:367
static Type listOf(Type element, bool optional=false)
A list type.
Definition value.hpp:335
bool isAny() const noexcept
Whether the type is any.
Definition value.hpp:342
std::expected< Json, std::string > parseJson(std::string_view text)
Parses JSON text into the document layer's JSON tree.
Definition value.hpp:752
TypeKind
The kinds of a Type.
Definition value.hpp:294
@ Quantity
quantity.
Definition value.hpp:300
@ Record
record: a record whose fields the document does not declare.
Definition value.hpp:303
@ Key
key: an int or a string.
Definition value.hpp:302
@ Decimal
decimal.
Definition value.hpp:299
@ Timestamp
timestamp.
Definition value.hpp:301
@ Any
Not known to the document: a query result, a reply, an element's value.
Definition value.hpp:295
glz::generic_u64 Json
The JSON tree the document layer reads and writes: glaze's generic value in its integer-preserving mo...
Definition value.hpp:35
Json valueToJson(Value const &value)
Writes a value in its JSON form: the inverse of valueFromJson, with a timestamp as ISO-8601 text.
Definition value.hpp:668
std::expected< Type, std::string > parseTypePrefix(std::string_view &text, int depth)
Parses one type at the front of text, advancing it.
Definition value.hpp:419
std::expected< Type, std::string > parseType(std::string_view text)
Parses a document type: bool, int, string, decimal, quantity, timestamp, key, record,...
Definition value.hpp:479
std::vector< Value > ValueList
A list value.
Definition value.hpp:126
ValueKind
The kinds a Value holds.
Definition value.hpp:132
@ Int
int: a 64-bit signed integer, exact.
Definition value.hpp:135
@ String
string: UTF-8 text.
Definition value.hpp:136
@ Quantity
quantity: a decimal with a unit.
Definition value.hpp:138
@ Record
A record of named fields.
Definition value.hpp:141
@ Decimal
decimal: an exact rational with declared places.
Definition value.hpp:137
@ Timestamp
timestamp: a UTC instant with millisecond precision.
Definition value.hpp:139
@ Null
No value: absent, not entered, or the result of an error.
Definition value.hpp:133
std::expected< Value, std::string > valueFromJson(Json const &json)
Reads a value from its JSON form, structurally.
Definition value.hpp:570
std::optional< std::expected< Value, std::string > > exactFromJson(Json::object_t const &object)
Reads the wire form of a decimal or quantity: an object of exactly num, den, dp (and unit).
Definition value.hpp:512
bool nestsTooDeep(std::string_view text)
Whether JSON text nests objects and arrays deeper than kMaxJsonDepth.
Definition value.hpp:721
constexpr std::size_t kMaxJsonDepth
The deepest a document's JSON may nest, counting each object and array as one level.
Definition value.hpp:714
std::map< std::string, Value, std::less<> > ValueRecord
A record value: field name to value, ordered by name.
Definition value.hpp:129
std::optional< std::int64_t > jsonInt(Json const &json) noexcept
Reads a JSON integer as an int64.
Definition value.hpp:496
std::string pointerAppend(std::string_view base, std::string_view token)
Appends one reference token to a JSON Pointer, escaping ~ and / as RFC 6901 requires.
Definition value.hpp:68
constexpr std::string_view kindName(ValueKind kind)
The name of a value kind, as a document spells its type.
Definition value.hpp:147
Node text(Text spec)
Static or bound text.
Definition view.hpp:1194
std::variant< std::int64_t, std::string > Key
The identity of a Switch case, a Select option, a ForEach or Table row and a drag payload.
Definition view.hpp:38