commons 0.1.5
Header-only C++23 library of common/shared types for the C++ libraries
Loading...
Searching...
No Matches
json.hpp
Go to the documentation of this file.
1#pragma once
2
39
40#include <commons/config.hpp>
41
42#if COMMONS_WITH_NLOHMANN_JSON
43
44#include <commons/color.hpp>
47#include <commons/flag.hpp>
48#include <commons/icon.hpp>
49#include <commons/id.hpp>
50#include <commons/origin.hpp>
52#include <commons/semver.hpp>
53#include <commons/types.hpp>
55
56#include <nlohmann/json.hpp>
57
58#include <array>
59#include <complex>
60#include <concepts>
61#include <cstddef>
62#include <exception>
63#include <optional>
64#include <string>
65#include <type_traits>
66#include <utility>
67
68namespace comms {
69
70// FixedString<N> ⇄ JSON string ----------------------------------------------
71
72template <std::size_t N>
73inline void to_json(::nlohmann::json& j, const FixedString<N>& s) {
74 j = std::string{s.view()};
75}
76
77template <std::size_t N>
78inline void from_json(const ::nlohmann::json& j, FixedString<N>& s) {
79 const auto str = j.template get<std::string>();
80 if (str.size() > FixedString<N>::size()) {
81 throw ::nlohmann::detail::other_error::create(
82 502,
83 "commons: string of length " + std::to_string(str.size()) +
84 " does not fit FixedString<" + std::to_string(N) + "> (capacity " +
85 std::to_string(FixedString<N>::size()) + ")",
86 &j);
87 }
88 std::size_t i = 0;
89 for (; i < str.size(); ++i) {
90 s.value[i] = str[i];
91 }
92 for (; i < N; ++i) {
93 s.value[i] = '\0';
94 }
95}
96
97// Color ⇄ JSON hex string ----------------------------------------------------
98
99inline void to_json(::nlohmann::json& j, const Color& c) {
100 j = c.to_hex_string();
101}
102
103inline void from_json(const ::nlohmann::json& j, Color& c) {
104 const auto str = j.template get<std::string>();
105 const auto parsed = Color::parse(str);
106 if (!parsed) {
107 throw ::nlohmann::detail::other_error::create(
108 502, "commons: '" + str + "' is not a valid color", &j);
109 }
110 c = *parsed;
111}
112
113// Icon ⇄ JSON set:name string ------------------------------------------------
114
115inline void to_json(::nlohmann::json& j, const Icon& i) {
116 j = i.to_string();
117}
118
119inline void from_json(const ::nlohmann::json& j, Icon& i) {
120 const auto str = j.template get<std::string>();
121 const auto parsed = Icon::parse(str);
122 if (!parsed) {
123 throw ::nlohmann::detail::other_error::create(
124 502, "commons: '" + str + "' is not a valid icon", &j);
125 }
126 i = *parsed;
127}
128
129// DisplayInfo ⇄ JSON object (absent fields are omitted) ----------------------
130
131inline void to_json(::nlohmann::json& j, const DisplayInfo& d) {
132 j = ::nlohmann::json::object();
133 if (d.name) {
134 j["name"] = *d.name;
135 }
136 if (d.description) {
137 j["description"] = *d.description;
138 }
139 if (d.icon) {
140 j["icon"] = *d.icon; // reuses Icon to_json
141 }
142 if (d.color) {
143 j["color"] = *d.color; // reuses Color to_json
144 }
145}
146
147inline void from_json(const ::nlohmann::json& j, DisplayInfo& d) {
148 d = DisplayInfo{};
149 if (const auto it = j.find("name"); it != j.end() && !it->is_null()) {
150 d.name = it->template get<std::string>();
151 }
152 if (const auto it = j.find("description"); it != j.end() && !it->is_null()) {
153 d.description = it->template get<std::string>();
154 }
155 if (const auto it = j.find("icon"); it != j.end() && !it->is_null()) {
156 d.icon = it->template get<Icon>(); // reuses Icon from_json (validates)
157 }
158 if (const auto it = j.find("color"); it != j.end() && !it->is_null()) {
159 d.color = it->template get<Color>(); // reuses Color from_json (validates)
160 }
161}
162
163// FlagRef ⇄ JSON name string -------------------------------------------------
164// Flags are compile-time types, so a name read back from JSON is resolved
165// against the GlobalFlagRegistry rather than reconstructing a type — analogous
166// to how Color/Icon validate on parse.
167
168inline void to_json(::nlohmann::json& j, const FlagRef& f) {
169 j = std::string{f.name};
170}
171
172inline void from_json(const ::nlohmann::json& j, FlagRef& f) {
173 const auto name = j.template get<std::string>();
174 const auto found = GlobalFlagRegistry::instance().find(name);
175 if (!found) {
176 throw ::nlohmann::detail::other_error::create(
177 502, "commons: '" + name + "' is not a registered flag", &j);
178 }
179 f = *found;
180}
181
182// FlagSet ⇄ JSON array of names ----------------------------------------------
183
184inline void to_json(::nlohmann::json& j, const FlagSet& s) {
185 j = ::nlohmann::json::array();
186 for (const auto& f : s) {
187 j.push_back(std::string{f.name});
188 }
189}
190
191inline void from_json(const ::nlohmann::json& j, FlagSet& s) {
192 s.clear();
193 for (const auto& elem : j) {
194 s.insert(elem.template get<FlagRef>());
195 }
196}
197
198// WithPriority<T> / PrioritizedSet<T> ⇄ JSON ---------------------------------
199// Gated on T being json-serializable so a non-serializable payload does not
200// break this header. A WithPriority travels as {"priority":N,"value":<T>}
201// (reusing T's own hooks for the value, the way DisplayInfo reuses Icon/Color);
202// a PrioritizedSet travels as a JSON array in sorted (ascending-priority) order.
203
204namespace detail {
205
206template <typename T>
207concept JsonSerializable = requires(::nlohmann::json& j, const T& v) { j = v; };
208
209template <typename T>
210concept JsonDeserializable = requires(const ::nlohmann::json& j, T& v) { j.get_to(v); };
211
212} // namespace detail
213
214template <typename T>
215 requires detail::JsonSerializable<T>
216inline void to_json(::nlohmann::json& j, const WithPriority<T>& w) {
217 j = ::nlohmann::json{{"priority", w.priority()}, {"value", w.value()}};
218}
219
220template <typename T>
221 requires detail::JsonDeserializable<T>
222inline void from_json(const ::nlohmann::json& j, WithPriority<T>& w) {
223 j.at("value").get_to(w.value());
224 w.set_priority(j.at("priority").template get<int>());
225}
226
227template <typename T>
228 requires detail::JsonSerializable<T>
229inline void to_json(::nlohmann::json& j, const PrioritizedSet<T>& s) {
230 j = ::nlohmann::json::array();
231 for (const auto& v : s) {
232 j.push_back(v); // already in sorted order
233 }
234}
235
236// from_json only when the priority can be recovered from the element itself;
237// otherwise the set is to_json-only (a plain T's priority is not persisted).
238template <typename T>
239 requires detail::JsonDeserializable<T> &&
240 (std::is_base_of_v<Prioritized, T> || Prioritizable<T>)
241inline void from_json(const ::nlohmann::json& j, PrioritizedSet<T>& s) {
242 if (!j.is_array()) {
243 throw ::nlohmann::detail::other_error::create(
244 502, "commons: PrioritizedSet expects a JSON array", &j);
245 }
246 s.clear();
247 for (const auto& elem : j) {
248 T value = elem.template get<T>();
249 const int p = get_priority(value); // re-snapshot from the element
250 s.insert(p, std::move(value));
251 }
252}
253
254// Hsl / Hsv ⇄ JSON objects ---------------------------------------------------
255
256inline void to_json(::nlohmann::json& j, const Hsl& c) {
257 j = ::nlohmann::json{{"h", c.h}, {"s", c.s}, {"l", c.l}, {"a", c.a}};
258}
259
260inline void from_json(const ::nlohmann::json& j, Hsl& c) {
261 c.h = j.at("h").template get<f64>();
262 c.s = j.at("s").template get<f64>();
263 c.l = j.at("l").template get<f64>();
264 c.a = j.at("a").template get<f64>();
265}
266
267inline void to_json(::nlohmann::json& j, const Hsv& c) {
268 j = ::nlohmann::json{{"h", c.h}, {"s", c.s}, {"v", c.v}, {"a", c.a}};
269}
270
271inline void from_json(const ::nlohmann::json& j, Hsv& c) {
272 c.h = j.at("h").template get<f64>();
273 c.s = j.at("s").template get<f64>();
274 c.v = j.at("v").template get<f64>();
275 c.a = j.at("a").template get<f64>();
276}
277
278// SemVer ⇄ JSON version string -----------------------------------------------
279
280inline void to_json(::nlohmann::json& j, const SemVer& v) {
281 j = v.to_string();
282}
283
284inline void from_json(const ::nlohmann::json& j, SemVer& v) {
285 const auto str = j.template get<std::string>();
286 const auto parsed = SemVer::parse(str);
287 if (!parsed) {
288 throw ::nlohmann::detail::other_error::create(
289 502, "commons: '" + str + "' is not a valid semantic version", &j);
290 }
291 v = *parsed;
292}
293
294// VersionConstraint ⇄ JSON range string --------------------------------------
295// parse() throws std::invalid_argument on a malformed sub-version; rewrap it as
296// a commons JSON error to match the rest of this file.
297
298inline void to_json(::nlohmann::json& j, const VersionConstraint& v) {
299 j = v.raw();
300}
301
302inline void from_json(const ::nlohmann::json& j, VersionConstraint& v) {
303 const auto str = j.template get<std::string>();
304 try {
306 } catch (const std::exception&) {
307 throw ::nlohmann::detail::other_error::create(
308 502, "commons: '" + str + "' is not a valid version constraint", &j);
309 }
310}
311
312// Id<Tag, Repr> ⇄ inner Repr's natural JSON ----------------------------------
313// Delegates straight to the wrapped representation's nlohmann handler — uint
314// reprs travel as JSON numbers, `std::string` as a JSON string, and `ulid::Ulid`
315// (when ULID is on) as the ULID-canonical string via its own to_json/from_json.
316
317template <class Tag, class Repr>
318inline void to_json(::nlohmann::json& j, const Id<Tag, Repr>& id) {
319 j = id.value();
320}
321
322template <class Tag, class Repr>
323inline void from_json(const ::nlohmann::json& j, Id<Tag, Repr>& id) {
324 id = Id<Tag, Repr>{j.template get<Repr>()};
325}
326
327// IOrigin ⇄ JSON object {"kind", ...fields} ----------------------------------
328// The base library carries no JSON, so the origin hooks live here. The four
329// built-in kinds round-trip their fields below; a custom origin kind supplies
330// its own to_json/from_json and (de)serializes its concrete type directly.
331// OriginPtr (a std::unique_ptr) is handled by an adl_serializer at the bottom.
332
333inline void to_json(::nlohmann::json& j, const CoreOrigin& o) {
334 j = ::nlohmann::json{{"kind", std::string{o.kind()}}};
335}
336inline void from_json(const ::nlohmann::json& /*j*/, CoreOrigin& /*o*/) {}
337
338inline void to_json(::nlohmann::json& j, const InternalOrigin& o) {
339 j = ::nlohmann::json{{"kind", std::string{o.kind()}}};
340}
341inline void from_json(const ::nlohmann::json& /*j*/, InternalOrigin& /*o*/) {}
342
343inline void to_json(::nlohmann::json& j, const ExternalOrigin& o) {
344 j = ::nlohmann::json{{"kind", std::string{o.kind()}}};
345 if (!o.source.empty()) {
346 j["source"] = o.source;
347 }
348}
349inline void from_json(const ::nlohmann::json& j, ExternalOrigin& o) {
350 if (const auto it = j.find("source"); it != j.end() && !it->is_null()) {
351 it->get_to(o.source);
352 }
353}
354
355inline void to_json(::nlohmann::json& j, const UnknownOrigin& o) {
356 j = ::nlohmann::json{{"kind", std::string{o.kind()}}};
357}
358inline void from_json(const ::nlohmann::json& /*j*/, UnknownOrigin& /*o*/) {}
359
360// Polymorphic write: dispatch on kind() to the matching built-in serializer; an
361// unrecognized (custom) kind falls back to writing just its discriminator.
362inline void to_json(::nlohmann::json& j, const IOrigin& o) {
363 if (const auto k = o.kind(); k == CoreOrigin::KIND) {
364 j = static_cast<const CoreOrigin&>(o);
365 } else if (k == InternalOrigin::KIND) {
366 j = static_cast<const InternalOrigin&>(o);
367 } else if (k == ExternalOrigin::KIND) {
368 j = static_cast<const ExternalOrigin&>(o);
369 } else if (k == UnknownOrigin::KIND) {
370 j = static_cast<const UnknownOrigin&>(o);
371 } else {
372 j = ::nlohmann::json{{"kind", std::string{k}}};
373 }
374}
375
376#if defined(COMMONS_HAS_INT128)
377
378namespace detail {
379
380[[nodiscard]] inline std::string u128_to_string(u128 v) {
381 if (v == 0) {
382 return "0";
383 }
384 // 2^128 has 39 decimal digits; 40 leaves room and avoids a reverse pass.
385 std::array<char, 40> buf{};
386 std::size_t pos = buf.size();
387 while (v > 0) {
388 buf[--pos] = static_cast<char>('0' + static_cast<int>(v % 10));
389 v /= 10;
390 }
391 return std::string{buf.data() + pos, buf.size() - pos};
392}
393
394[[nodiscard]] inline std::string i128_to_string(const i128 v) {
395 const bool negative = v < 0;
396 // Form the magnitude in unsigned space so INT128_MIN is handled correctly.
397 const u128 mag = negative ? (~static_cast<u128>(v) + 1) : static_cast<u128>(v);
398 std::string digits = u128_to_string(mag);
399 return negative ? "-" + digits : digits;
400}
401
402// Parse a decimal JSON string into a u128, validating and range-checking as we
403// go. Plain numbers narrow through `long long` and lose precision above 2^64,
404// so the wire form is a string; every failure surfaces as a commons error (the
405// project analog of the rest of this file's `other_error::create(502, ...)`).
406template <typename BasicJsonType>
407[[nodiscard]] u128 json_to_u128(const BasicJsonType& j) {
408 if (!j.is_string()) {
409 throw ::nlohmann::detail::other_error::create(
410 502, "commons: u128 must be encoded as a JSON string", &j);
411 }
412 const auto& s = j.template get_ref<const std::string&>();
413 if (s.empty()) {
414 throw ::nlohmann::detail::other_error::create(502, "commons: u128 string is empty", &j);
415 }
416 constexpr u128 u128_max = ~static_cast<u128>(0);
417 u128 v = 0;
418 for (const char c : s) {
419 if (c < '0' || c > '9') {
420 throw ::nlohmann::detail::other_error::create(
421 502, "commons: non-digit character in u128 string", &j);
422 }
423 const auto d = static_cast<u128>(c - '0');
424 if (v > (u128_max - d) / 10) {
425 throw ::nlohmann::detail::other_error::create(
426 502, "commons: u128 string out of range", &j);
427 }
428 v = v * 10 + d;
429 }
430 return v;
431}
432
433// Parse a signed decimal JSON string into an i128. Accepts an optional leading
434// `+`/`-`; the magnitude is accumulated in unsigned space so `i128_min` (whose
435// positive counterpart does not fit in i128) round-trips without overflow.
436template <typename BasicJsonType>
437[[nodiscard]] i128 json_to_i128(const BasicJsonType& j) {
438 if (!j.is_string()) {
439 throw ::nlohmann::detail::other_error::create(
440 502, "commons: i128 must be encoded as a JSON string", &j);
441 }
442 const auto& s = j.template get_ref<const std::string&>();
443 if (s.empty()) {
444 throw ::nlohmann::detail::other_error::create(502, "commons: i128 string is empty", &j);
445 }
446 std::size_t i = 0;
447 bool negative = false;
448 if (s[0] == '-') {
449 negative = true;
450 i = 1;
451 } else if (s[0] == '+') {
452 i = 1;
453 }
454 if (i == s.size()) {
455 throw ::nlohmann::detail::other_error::create(
456 502, "commons: i128 string has sign but no digits", &j);
457 }
458 // Permissible magnitudes: 0 .. 2^127 when negative (i128_min),
459 // 0 .. 2^127 - 1 when non-negative.
460 constexpr u128 neg_mag_max = static_cast<u128>(1) << 127;
461 constexpr u128 pos_mag_max = neg_mag_max - 1;
462 u128 mag = 0;
463 for (; i < s.size(); ++i) {
464 const char c = s[i];
465 if (c < '0' || c > '9') {
466 throw ::nlohmann::detail::other_error::create(
467 502, "commons: non-digit character in i128 string", &j);
468 }
469 const auto d = static_cast<u128>(c - '0');
470 if (mag > (neg_mag_max - d) / 10) {
471 throw ::nlohmann::detail::other_error::create(
472 502, "commons: i128 string out of range", &j);
473 }
474 mag = mag * 10 + d;
475 }
476 if (!negative && mag > pos_mag_max) {
477 throw ::nlohmann::detail::other_error::create(502, "commons: i128 string out of range", &j);
478 }
479 if (negative) {
480 // Map magnitude → negative without signed overflow on i128_min (mag ==
481 // 2^127): v = -mag, computed as -(mag - 1) - 1.
482 const u128 mm1 = mag - 1;
483 const i128 partial = -static_cast<i128>(mm1);
484 return partial - 1;
485 }
486 return static_cast<i128>(mag);
487}
488
489} // namespace detail
490
491#endif // COMMONS_HAS_INT128
492
493} // namespace comms
494
495#if defined(COMMONS_HAS_INT128)
496
497// 128-bit integers are fundamental types, so ADL cannot find a `to_json` /
498// `from_json` for them in namespace `comms`. Specialize nlohmann's serializer
499// directly instead. They travel as decimal strings to avoid lossy narrowing.
500namespace nlohmann {
501
502template <>
503struct adl_serializer<::comms::i128> {
504 template <typename BasicJsonType>
505 static void to_json(BasicJsonType& j, const ::comms::i128 v) {
506 j = ::comms::detail::i128_to_string(v);
507 }
508
509 template <typename BasicJsonType>
510 static void from_json(const BasicJsonType& j, ::comms::i128& v) {
511 v = ::comms::detail::json_to_i128(j);
512 }
513};
514
515template <>
516struct adl_serializer<::comms::u128> {
517 template <typename BasicJsonType>
518 static void to_json(BasicJsonType& j, const ::comms::u128 v) {
519 j = ::comms::detail::u128_to_string(v);
520 }
521
522 template <typename BasicJsonType>
523 static void from_json(const BasicJsonType& j, ::comms::u128& v) {
524 v = ::comms::detail::json_to_u128(j);
525 }
526};
527
528} // namespace nlohmann
529
530#endif // COMMONS_HAS_INT128
531
532// std::complex<T> lives in namespace `std`, so ADL cannot find a `to_json` /
533// `from_json` for the complex aliases (cs8…cs64, cu8…cu64, cf32/cf64) in
534// namespace `comms`. Specialize nlohmann's serializer instead. They travel as a
535// two-element JSON array [real, imaginary]; the component type T serializes
536// natively.
537template <typename T>
538struct nlohmann::adl_serializer<std::complex<T>> {
539 template <typename BasicJsonType>
540 static void to_json(BasicJsonType& j, const std::complex<T>& c) {
541 j = BasicJsonType::array();
542 j.push_back(c.real());
543 j.push_back(c.imag());
544 }
545
546 template <typename BasicJsonType>
547 static void from_json(const BasicJsonType& j, std::complex<T>& c) {
548 c.real(j.at(0).template get<T>());
549 c.imag(j.at(1).template get<T>());
550 }
551}; // namespace nlohmann
552
553// std::optional<T> lives in namespace `std`, so (like std::complex) ADL cannot
554// find a `to_json` / `from_json` for it in namespace `comms`. Specialize
555// nlohmann's serializer instead: `nullopt` round-trips as JSON `null`, and a
556// held value travels via the wrapped T's own serializer.
557template <typename T>
558struct nlohmann::adl_serializer<std::optional<T>> {
559 template <typename BasicJsonType>
560 static void to_json(BasicJsonType& j, const std::optional<T>& opt) {
561 if (opt.has_value()) {
562 j = *opt;
563 } else {
564 j = nullptr;
565 }
566 }
567
568 template <typename BasicJsonType>
569 static void from_json(const BasicJsonType& j, std::optional<T>& opt) {
570 if (j.is_null()) {
571 opt = std::nullopt;
572 } else {
573 opt = j.template get<T>();
574 }
575 }
576}; // namespace nlohmann
577
578// comms::OriginPtr is a std::unique_ptr<IOrigin>, which lives in namespace `std`,
579// so (like std::optional) ADL cannot find a to_json/from_json for it in namespace
580// comms. Specialize nlohmann's serializer: a null pointer round-trips as JSON
581// null; a held origin writes via its polymorphic to_json and reads back by
582// resolving "kind" against the GlobalOriginRegistry (unknown kind throws).
583template <>
584struct nlohmann::adl_serializer<::comms::OriginPtr> {
585 template <typename BasicJsonType>
586 static void to_json(BasicJsonType& j, const ::comms::OriginPtr& o) {
587 if (o) {
588 j = *o; // polymorphic to_json(const IOrigin&)
589 } else {
590 j = nullptr;
591 }
592 }
593
594 template <typename BasicJsonType>
595 static void from_json(const BasicJsonType& j, ::comms::OriginPtr& o) {
596 if (j.is_null()) {
597 o = nullptr;
598 return;
599 }
600 if (!j.is_object()) {
601 throw ::nlohmann::detail::other_error::create(
602 502, "commons: origin must be a JSON object", &j);
603 }
604 const auto it = j.find("kind");
605 if (it == j.end()) {
606 throw ::nlohmann::detail::other_error::create(
607 502, "commons: origin missing 'kind'", &j);
608 }
609 const auto kind = it->template get<std::string>();
610 auto created = ::comms::GlobalOriginRegistry::instance().create(kind);
611 if (!created) {
612 throw ::nlohmann::detail::other_error::create(
613 502, "commons: unknown origin kind '" + kind + "'", &j);
614 }
615 // Restore the built-in kinds' fields; a custom kind comes back field-less
616 // (its owner (de)serializes the concrete type with its own hooks).
617 if (kind == ::comms::ExternalOrigin::KIND) {
618 j.get_to(static_cast<::comms::ExternalOrigin&>(*created));
619 }
620 o = std::move(created);
621 }
622}; // namespace nlohmann
623
624#endif // COMMONS_WITH_NLOHMANN_JSON
The definition came from an external source, named by source.
Definition origin.hpp:137
static constexpr std::string_view KIND
Compile-time discriminator.
Definition origin.hpp:93
static VersionConstraint parse(const std::string_view s)
Parse a range string into a constraint.
Definition version_constraint.hpp:65
A tiny RGBA color container with rich, mostly-constexpr color manipulation, plus the Hsl/Hsv model st...
Central feature-gate header for Commons' optional integrations.
Presentation metadata for a type/value — name, description, icon, color, all optional — plus the trai...
NTTP-friendly fixed-size string usable as a non-type template parameter.
Compile-time named flags grouped into categories, plus a runtime FlagSet, a program-wide GlobalFlagRe...
A tiny value type carrying an Iconify icon identifier such as mdi:abacus (a set:name pair).
A strong-typed identifier — comms::Id<Tag, Repr> — that wraps an allowed representation in a phantom ...
A polymorphic provenance envelope — comms::IOrigin — that records where a definition came from,...
std::unique_ptr< IOrigin > OriginPtr
A heap-owned origin. The canonical way to carry an IOrigin by value.
Definition origin.hpp:79
Priorities for orderable things (adapters, transports, …) plus the helpers to sort them deterministic...
int get_priority(const T &value) noexcept
Priority of a value or reference.
Definition prioritized.hpp:164
A value type for a Semantic Versioning 2.0.0 version — major.minor.patch with optional prerelease and...
static constexpr std::optional< Color > parse(std::string_view s)
Parse any supported textual color: hex (see parse_hex), CSS-functional rgb()/rgba()/hsl()/hsla(),...
Definition color.hpp:1327
static constexpr std::optional< Icon > parse(const std::string_view value)
Non-throwing validation: returns the Icon for a well-formed value (exactly one :, non-empty set and n...
Definition icon.hpp:96
static std::optional< SemVer > parse(std::string_view s)
Non-throwing parse.
Definition semver.hpp:67
Fixed-width numeric aliases shared across the C++ libraries.
A semver range constraint that answers satisfies(SemVer).