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