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