libQuotient
A Qt library for building matrix clients
Loading...
Searching...
No Matches
converters.h
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net>
2// SPDX-License-Identifier: LGPL-2.1-or-later
3
4#pragma once
5
7
8#include <QtCore/QDate>
9#include <QtCore/QJsonArray> // Includes <QtCore/QJsonValue>
10#include <QtCore/QJsonDocument>
11#include <QtCore/QJsonObject>
12#include <QtCore/QSet>
13#include <QtCore/QTimeZone>
14#include <QtCore/QUrlQuery>
15#include <QtCore/QVector>
16
17#include <array>
18#include <optional>
19#include <type_traits>
20#include <variant>
21#include <vector>
22
23class QVariant;
24
25namespace Quotient {
26
27inline void editSubobject(QJsonObject& json, auto key, std::invocable<QJsonObject&> auto visitor)
28{
29 auto subObject = json.take(key).toObject();
30 visitor(subObject);
31 json.insert(key, subObject);
32}
33
34inline void replaceSubvalue(QJsonObject& json, auto topLevelKey, auto subKey, QJsonValue subValue)
35{
36 editSubobject(json, topLevelKey, [subKey, subValue](QJsonObject& innerJson) {
37 innerJson.insert(subKey, subValue);
38 });
39}
40
41template <typename T>
42struct JsonObjectConverter;
43// Specialisations should implement either or both of:
44//static void dumpTo(QJsonObject&, const T&); // For toJson() and fillJson() to work
45//static void fillFrom(const QJsonObject&, T&); // For fromJson() and fillFromJson() to work
46
47template <typename PodT>
48PodT fromJson(const auto&);
49
50template <typename T>
52 // By default, revert to fromJson() so that one could provide a single
53 // fromJson<T, QJsonObject> specialisation instead of specialising
54 // the entire JsonConverter; if a different type of JSON value is needed
55 // (e.g., an array), specialising JsonConverter is inevitable
56 static T load(const QJsonValue& jv) { return fromJson<T>(jv.toObject()); }
57 static T load(const QJsonDocument& jd) { return fromJson<T>(jd.object()); }
58};
59
60//! \brief The switchboard for extra conversion algorithms behind from/toJson
61//!
62//! This template is mainly intended for partial conversion specialisations
63//! since from/toJson are functions and cannot be partially specialised.
64//! Another case for JsonConverter is to insulate types that can be constructed
65//! from basic types - namely, QVariant and QUrl can be directly constructed
66//! from QString and having an overload or specialisation for those leads to
67//! ambiguity between these and QJsonValue. For trivial (converting
68//! QJsonObject/QJsonValue) and most simple cases such as primitive types or
69//! QString this class is not needed.
70//!
71//! Do NOT call the functions of this class directly unless you know what you're
72//! doing; and do not try to specialise basic things unless you're really sure
73//! that they are not supported and it's not feasible to support those by means
74//! of overloading toJson() and specialising fromJson().
75template <typename T>
77 static_assert(std::is_class_v<T> || std::is_union_v<T>,
78 "The default JsonConverter definition only supports class types; "
79 "check the constraints on the specialisation you intended to invoke");
80
81 static auto dump(const T& data)
82 {
83 if constexpr (requires() { data.toJson(); })
84 return data.toJson();
85 else {
86 QJsonObject jo;
87 JsonObjectConverter<T>::dumpTo(jo, data);
88 return jo;
89 }
90 }
91
92 using JsonObjectUnpacker<T>::load;
93 static T load(const QJsonObject& jo)
94 {
95 // 'else' below are required to suppress code generation for unused
96 // branches - 'return' is not enough
97 if constexpr (std::is_same_v<T, QJsonObject>)
98 return jo;
99 else if constexpr (std::is_constructible_v<T, QJsonObject>)
100 return T(jo);
101 else {
102 T pod;
103 JsonObjectConverter<T>::fillFrom(jo, pod);
104 return pod;
105 }
106 }
107};
108
109template <typename T>
110inline auto toJson(const T& pod)
111// -> can return anything from which QJsonValue or, in some cases, QJsonDocument
112// is constructible
113{
114 if constexpr (std::is_constructible_v<QJsonValue, T>)
115 return pod; // No-op if QJsonValue can be directly constructed
116 else
117 return JsonConverter<T>::dump(pod);
118}
119
120template <typename T>
121inline void fillJson(QJsonObject& json, const T& data)
122{
123 JsonObjectConverter<T>::dumpTo(json, data);
124}
125
126template <typename PodT>
127inline PodT fromJson(const auto& json)
128{
129 // JsonT here can be whatever the respective JsonConverter specialisation
130 // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject
131 return JsonConverter<PodT>::load(json);
132}
133
134// Convenience fromJson() overload that deduces PodT instead of requiring
135// the coder to explicitly type it. It still enforces the
136// overwrite-everything semantics of fromJson(), unlike fillFromJson()
137
138template <typename PodT>
139inline void fromJson(const auto& json, PodT& pod)
140{
141 pod = fromJson<PodT>(json);
142}
143
144template <typename T>
145inline void fillFromJson(const QJsonValue& jv, T& pod)
146{
147 if constexpr (requires() { JsonObjectConverter<T>::fillFrom({}, pod); }) {
148 JsonObjectConverter<T>::fillFrom(jv.toObject(), pod);
149 return;
150 } else if (!jv.isUndefined())
151 pod = fromJson<T>(jv);
152}
153
154namespace _impl {
155 QUOTIENT_API void reportEnumOutOfBounds(uint32_t v, const char* enumTypeName);
156}
157
158template <typename EnumT>
159inline std::optional<EnumT> enumFromJsonString(const QString& s, const auto& enumValues)
160{
161 static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>);
162 if (const auto it = std::ranges::find(enumValues, s); it != cend(enumValues))
163 return static_cast<EnumT>(it - cbegin(enumValues));
164
165 return std::nullopt;
166}
167
168template <typename EnumT>
169inline QString enumToJsonString(EnumT v, const auto& enumValues)
170{
171 static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>);
172 const auto intV = std::to_underlying(v);
173 if (QUO_CHECK(intV < size(enumValues)))
174 return enumValues[intV];
175
176 _impl::reportEnumOutOfBounds(intV, qt_getEnumName(EnumT()));
177 return {};
178}
179
180template <typename FlagT>
181inline std::optional<FlagT> flagFromJsonString(const QString& s, const auto& flagValues)
182{
183 // Enums based on signed integers don't make much sense for flag types
184 static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>);
185 if (const auto it = std::ranges::find(flagValues, s); it != cend(flagValues))
186 return static_cast<FlagT>(1U << (it - cbegin(flagValues)));
187
188 return std::nullopt;
189}
190
191template <typename FlagT>
192inline QString flagToJsonString(FlagT v, const auto& flagValues)
193{
194 static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>);
195 const auto intV = std::to_underlying(v);
196 if (const auto offset = std::countr_zero(intV); QUO_CHECK(offset < ssize(flagValues)))
197 return flagValues[offset];
198
199 _impl::reportEnumOutOfBounds(intV, qt_getEnumName(FlagT()));
200 return {};
201}
202
203// Specialisations
204
205template <>
206inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); }
207
208template <>
209inline int fromJson(const QJsonValue& jv) { return jv.toInt(); }
210
211template <>
212inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); }
213
214template <>
215inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); }
216
217template <>
218inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); }
219
220template <>
221inline QString fromJson(const QJsonValue& jv) { return jv.toString(); }
222
223//! \brief JSON conversions for (non-flag) enums
224//!
225//! This specialisation gets applied to any enumeration (in fact - any type at all) for which
226//! there is a specialisation of MetaEnum template such that Serializable_Enum is satisfied. See
227//! the documentation for MetaEnum for the
228template <Serializable_Enum EnumT>
229struct JsonConverter<EnumT>
230{
231 //! \return if \p jv has a string, the ordinal (0-based) index of it in
232 //! `MetaEnum<EnumT>::strings`; or `MetaEnum<EnumT>::defaultValue` if the string is
233 //! not found in `MetaEnum<EnumT>::strings`, or \p jv doesn't hold a string
234 static auto load(const QJsonValue &jv)
235 {
238 }
239
240 //! \return the string found at \p val in `MetaEnum<EnumT>::strings`; \p val must correspond to
241 //! a valid 0-based index into `MetaEnum<EnumT>::strings` (i.e. it's between 0 and
242 //! `std::size(MetaEnum<EnumT>::strings) - 1`, inclusive).
243 static auto dump(EnumT val) { return enumToJsonString(val, MetaEnum<EnumT>::strings); }
244};
245
246template <Serializable_Flag FlagT>
247struct JsonConverter<FlagT>
248{
249 //! \return if \p jv has a string, the index of it in `MetaEnum<FlagT>::strings`, power of 2
250 //! (i.e. 1 for 0-th entry, 2 for the first entry, 4 for the second one and so on);
251 //! or `MetaEnum<FlagT>::defaultValue` if the string is not found in
252 //! `MetaEnum<FlagT>::strings`, or \p jv doesn't hold a string
253 static auto load(const QJsonValue &jv)
254 {
257 }
258
259 //! \return the string found at `\p val log 2 in` `MetaEnum<FlagT>::strings` (i.e. 0-th string
260 //! for 1, first one for 2, second for 4 and so on); \p val must be a power of 2 for
261 //! a valid index into `MetaEnum<FlagT>::strings` - i.e. \p val must one of
262 //! 1, 2, 4,..., `2 ^ (std::size(MetaEnum<FlagT>::strings) - 1)`, inclusive
263 static auto dump(FlagT val) { return flagToJsonString(val, MetaEnum<FlagT>::strings); }
264};
265
266//! Use fromJson<QString> and then toLatin1()/toUtf8()/... to make QByteArray
267//!
268//! QJsonValue can only convert to QString and there's ambiguity whether
269//! conversion to QByteArray should use (fast but very limited) toLatin1() or
270//! (all encompassing and conforming to the JSON spec but slow) toUtf8().
271template <>
272inline QByteArray fromJson(const QJsonValue& jv) = delete;
273
274template <>
275inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); }
276
277template <>
278inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); }
279
280inline QJsonValue toJson(const QDateTime& val)
281{
282 return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue();
283}
284template <>
285inline QDateTime fromJson(const QJsonValue& jv)
286{
287 return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), QTimeZone::UTC);
288}
289
290inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); }
291template <>
292inline QDate fromJson(const QJsonValue& jv)
293{
294 return fromJson<QDateTime>(jv).date();
295}
296
297// Insulate QVariant and QUrl conversions into JsonConverter so that they don't
298// interfere with toJson(const QJsonValue&) over QString, since both types are
299// constructible from QString (even if QUrl requires explicit construction).
300
301template <>
302struct JsonConverter<QUrl> {
303 static auto load(const QJsonValue& jv)
304 {
305 return QUrl(jv.toString());
306 }
307 static auto dump(const QUrl& url)
308 {
309 return url.toString(QUrl::FullyEncoded);
310 }
311};
312
313template <>
315 static QJsonValue dump(const QVariant& v);
316 static QVariant load(const QJsonValue& jv);
317};
318
319template <typename... Ts>
320inline QJsonValue toJson(const std::variant<Ts...>& v)
321{
322 // std::visit requires all overloads to return the same type - and
323 // QJsonValue is a perfect candidate for that same type (assuming that
324 // variants never occur on the top level in Matrix API)
325 return std::visit(
326 [](const auto& value) { return QJsonValue { toJson(value) }; }, v);
327}
328
329template <typename T>
330struct JsonConverter<std::variant<QString, T>> {
331 static std::variant<QString, T> load(const QJsonValue& jv)
332 {
333 if (jv.isString())
334 return fromJson<QString>(jv);
335 return fromJson<T>(jv);
336 }
337};
338
339template <typename T>
340struct JsonConverter<std::optional<T>> {
341 static QJsonValue dump(const std::optional<T>& from)
342 {
343 return from.has_value() ? toJson(*from) : QJsonValue();
344 }
345 static std::optional<T> load(const QJsonValue& jv)
346 {
347 if (jv.isUndefined() || jv.isNull())
348 return std::nullopt;
349 return { fromJson<T>(jv) };
350 }
351};
352
353template <typename ContT>
355 static auto dump(const ContT& vals)
356 {
357 QJsonArray ja;
358 for (const auto& v : vals)
359 ja.push_back(toJson(v));
360 return ja;
361 }
362 static auto load(const QJsonArray& ja)
363 {
364 ContT vals;
365 vals.reserve(static_cast<typename ContT::size_type>(ja.size()));
366 // NB: Make sure fromJson<> gets QJsonValue (not QJsonValue*Ref)
367 // to avoid it falling back to the generic implementation that treats
368 // everything as an object. See also the message of commit 20f01303b
369 // that introduced these lines.
370 for (const auto& v : ja)
371 vals.push_back(fromJson<typename ContT::value_type, QJsonValue>(v));
372 return vals;
373 }
374 static auto load(const QJsonValue& jv) { return load(jv.toArray()); }
375 static auto load(const QJsonDocument& jd) { return load(jd.array()); }
376};
377
378template <typename T>
379struct JsonConverter<std::vector<T>>
380 : public JsonArrayConverter<std::vector<T>> {};
381
382template <typename T, size_t N>
383struct JsonConverter<std::array<T, N>> {
384 // The size of std::array is known at compile-time and those arrays
385 // are usually short. The common conversion logic therefore is to expand
386 // the passed source array into a pack of values converted with to/fromJson
387 // and then construct the target array list-initialised with that pack.
388 // For load(), this implies that if QJsonArray is not of the right size,
389 // the resulting std::array will not have extra values or will have empty
390 // values at the end - silently.
391 static constexpr std::make_index_sequence<N> Indices{};
392 template <typename TargetT, size_t... I>
393 static auto staticTransform(const auto& source, std::index_sequence<I...>,
394 auto unaryFn)
395 {
396 return TargetT { unaryFn(source[I])... };
397 }
398 static auto dump(const std::array<T, N> a)
399 {
400 return staticTransform<QJsonArray>(a, Indices, [](const T& v) {
401 return toJson(v);
402 });
403 }
404 static auto load(const QJsonArray& ja)
405 {
406 return staticTransform<std::array<T, N>>(ja, Indices,
407 fromJson<T, QJsonValue>);
408 }
409};
410
411template <typename T>
412struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {};
413
414template <>
416 static auto dump(const QStringList& sl)
417 {
419 }
420};
421
422template <>
424 static void dumpTo(QJsonObject& json, const QSet<QString>& s)
425 {
426 for (const auto& e : s)
428 }
429 static void fillFrom(const QJsonObject& json, QSet<QString>& s)
430 {
431 s.reserve(s.size() + json.size());
432 for (auto it = json.begin(); it != json.end(); ++it)
433 s.insert(it.key());
434 }
435};
436
437template <typename HashMapT>
439 static void dumpTo(QJsonObject& json, const HashMapT& hashMap)
440 {
441 for (auto it = hashMap.begin(); it != hashMap.end(); ++it)
442 json.insert(it.key(), toJson(it.value()));
443 }
444 static void fillFrom(const QJsonObject& jo, HashMapT& h)
445 {
446 h.reserve(h.size() + jo.size());
447 // NB: coercing the passed value to QJsonValue below is for
448 // the same reason as in JsonArrayConverter
449 for (auto it = jo.begin(); it != jo.end(); ++it)
450 h[it.key()] = fromJson<typename HashMapT::mapped_type, QJsonValue>(
451 it.value());
452 }
453};
454
455template <typename T, typename HashT>
458
459template <typename T>
461 : public HashMapFromJson<QHash<QString, T>> {};
462
463QJsonObject QUOTIENT_API toJson(const QVariantHash& vh);
464template <>
465QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv);
466
467// Conditional insertion into a QJsonObject
468
469constexpr bool IfNotEmpty = false;
470
471namespace _impl {
472 template <typename KeyT, typename ValT>
473 inline void addTo(QJsonObject& o, KeyT&& k, ValT&& v)
474 {
475 o.insert(std::forward<KeyT>(k), toJson(std::forward<ValT>(v)));
476 }
477
478 inline void addTo(QUrlQuery& q, const QString& k, auto v)
479 requires requires { u"%1"_s.arg(v); }
480 {
481 q.addQueryItem(k, u"%1"_s.arg(v));
482 }
483
484 // OpenAPI is entirely JSON-based, which means representing bools as
485 // textual true/false, rather than 1/0.
486 inline void addTo(QUrlQuery& q, const QString& k, bool v)
487 {
488 q.addQueryItem(k, v ? u"true"_s : u"false"_s);
489 }
490
491 inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v)
492 {
493 q.addQueryItem(k, QString::fromLatin1(v.toEncoded()));
494 }
495
496 inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals)
497 {
498 for (const auto& v : vals)
499 q.addQueryItem(k, v);
500 }
501
502 template <typename ValT>
503 inline void addTo(QUrlQuery& q, const QString&, const QHash<QString, ValT>& fields)
504 {
505 for (const auto& [k, v] : fields.asKeyValueRange())
506 addTo(q, k, v);
507 }
508
509 // This one is for types that don't have isEmpty() and for all types
510 // when Force is true
511 template <typename ValT, bool Force = true>
512 struct AddNode {
513 template <typename KeyT, typename ForwardedT>
514 static void impl(auto& container, KeyT&& key, ForwardedT&& value)
515 {
516 addTo(container, std::forward<KeyT>(key), std::forward<ForwardedT>(value));
517 }
518 };
519
520 // This one is for types that have isEmpty() when Force is false
521 template <typename ValT>
522 requires requires(ValT v) { v.isEmpty(); }
523 struct AddNode<ValT, IfNotEmpty> {
524 template <typename KeyT, typename ForwardedT>
525 static void impl(auto& container, KeyT&& key, ForwardedT&& value)
526 {
527 if (!value.isEmpty())
528 addTo(container, std::forward<KeyT>(key), std::forward<ForwardedT>(value));
529 }
530 };
531
532 // This one unfolds optionals (also only when IfNotEmpty is requested)
533 template <typename ValT>
534 struct AddNode<std::optional<ValT>, IfNotEmpty> {
535 template <typename KeyT>
536 static void impl(auto& container, KeyT&& key, const auto& optValue)
537 {
538 if (optValue)
539 addTo(container, std::forward<KeyT>(key), *optValue);
540 }
541 };
542} // namespace _impl
543
544/*! Add a key-value pair to QJsonObject or QUrlQuery
545 *
546 * Adds a key-value pair(s) specified by \p key and \p value to
547 * \p container, optionally (in case IfNotEmpty is passed for the first
548 * template parameter) taking into account the value "emptiness".
549 * With IfNotEmpty, \p value is NOT added to the container if and only if:
550 * - it has a method `isEmpty()` and `value.isEmpty() == true`, or
551 * - it's an optional that has no value (`nullopt`).
552 *
553 * If \p container is a QUrlQuery, an attempt to fit \p value into it is
554 * made as follows:
555 * - if \p value is a QJsonObject, \p key is ignored and pairs from \p value
556 * are copied to \p container, assuming that the value in each pair
557 * is a string;
558 * - if \p value is a QStringList, it is "exploded" into a list of key-value
559 * pairs with key equal to \p key and value taken from each list item;
560 * - if \p value is a bool, its OpenAPI (i.e. JSON) representation is added
561 * to the query (`true` or `false`, respectively).
562 *
563 * \tparam Force add the pair even if the value is empty. This is true
564 * by default; passing IfNotEmpty or false for this parameter
565 * enables emptiness checks as described above
566 */
567template <bool Force = true, typename KeyT, typename ValT>
568inline void addParam(auto& container, KeyT&& key, ValT&& value)
569{
570 _impl::AddNode<std::decay_t<ValT>, Force>::impl(container, std::forward<KeyT>(key),
571 std::forward<ValT>(value));
572}
573
574// This is a facility function to convert camelCase method/variable names
575// used throughout Quotient to snake_case JSON keys - see usage in
576// single_key_value.h and event.h (QUO_CONTENT_GETTER macro).
577inline auto toSnakeCase(QLatin1String s)
578{
579 QString result { s };
580 for (auto it = result.begin(); it != result.end(); ++it)
581 if (it->isUpper()) {
582 const auto offset = static_cast<int>(it - result.begin());
583 result.insert(offset, u'_'); // NB: invalidates iterators
584 it = result.begin() + offset + 1;
585 *it = it->toLower();
586 }
587 return result;
588}
589} // namespace Quotient
auto toSnakeCase(QLatin1String s)
Definition converters.h:577
QString flagToJsonString(FlagT v, const auto &flagValues)
Definition converters.h:192
QString enumToJsonString(EnumT v, const auto &enumValues)
Definition converters.h:169
void fillJson(QJsonObject &json, const T &data)
Definition converters.h:121
void fromJson(const auto &json, PodT &pod)
Definition converters.h:139
QJsonValue toJson(const QDateTime &val)
Definition converters.h:280
auto toJson(const T &pod)
Definition converters.h:110
void fillFromJson(const QJsonValue &jv, T &pod)
Definition converters.h:145
void addParam(auto &container, KeyT &&key, ValT &&value)
Definition converters.h:568
std::optional< EnumT > enumFromJsonString(const QString &s, const auto &enumValues)
Definition converters.h:159
QJsonValue toJson(const std::variant< Ts... > &v)
Definition converters.h:320
void editSubobject(QJsonObject &json, auto key, std::invocable< QJsonObject & > auto visitor)
Definition converters.h:27
void replaceSubvalue(QJsonObject &json, auto topLevelKey, auto subKey, QJsonValue subValue)
Definition converters.h:34
bool fromJson(const QJsonValue &jv)
Definition converters.h:206
std::optional< FlagT > flagFromJsonString(const QString &s, const auto &flagValues)
Definition converters.h:181
PodT fromJson(const auto &)
Definition converters.h:127
constexpr bool IfNotEmpty
Definition converters.h:469
#define QUOTIENT_API
static void dumpTo(QJsonObject &json, const HashMapT &hashMap)
Definition converters.h:439
static void fillFrom(const QJsonObject &jo, HashMapT &h)
Definition converters.h:444
static auto dump(const ContT &vals)
Definition converters.h:355
static auto load(const QJsonArray &ja)
Definition converters.h:362
The switchboard for extra conversion algorithms behind from/toJson.
Definition converters.h:76
static T load(const QJsonObject &jo)
Definition converters.h:93
static auto dump(const T &data)
Definition converters.h:81
static T load(const QJsonValue &jv)
Definition converters.h:56
#define QUO_CHECK(...)
Evaluate the boolean expression and, in Debug mode, assert it to be true.
Definition util.h:90