libQuotient
A Qt library for building matrix clients
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 
16 #include <type_traits>
17 #include <vector>
18 #include <array>
19 #include <variant>
20 #include <optional>
21 
22 class QVariant;
23 
24 namespace Quotient {
25 template <typename T>
26 struct 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 
32 template <typename PodT, typename JsonT>
33 PodT fromJson(const JsonT&);
34 
35 template <typename T>
36 struct 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().
60 template <typename T>
61 struct 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 
90 template <typename T>
91 inline 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 
101 template <typename T>
102 inline void fillJson(QJsonObject& json, const T& data)
103 {
104  JsonObjectConverter<T>::dumpTo(json, data);
105 }
106 
107 template <typename PodT, typename JsonT>
108 inline 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 
119 template <typename JsonT, typename PodT>
120 inline void fromJson(const JsonT& json, PodT& pod)
121 {
122  pod = fromJson<PodT>(json);
123 }
124 
125 template <typename T>
126 inline 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 
135 namespace _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
149 template <typename EnumT, typename EnumStringValuesT>
150 EnumT 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 static_cast<EnumT>(it - cbegin(enumValues));
157 
158  if (!s.isEmpty())
159  _impl::warnUnknownEnumValue(s, 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
172 template <typename EnumT, typename EnumStringValuesT>
173 QString 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(static_cast<uint32_t>(v),
180  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
195 template <typename FlagT, typename FlagStringValuesT>
196 FlagT 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::ranges::find(flagValues, s); it != cend(flagValues))
202  return static_cast<FlagT>(1U << (it - cbegin(flagValues)));
203 
204  if (!s.isEmpty())
205  _impl::warnUnknownEnumValue(s, qt_getEnumName(FlagT()));
206  return defaultValue;
207 }
208 
209 template <typename FlagT, typename FlagStringValuesT>
210 QString flagToJsonString(FlagT v, const FlagStringValuesT& flagValues)
211 {
212  static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>);
213  if (const auto offset = std::countr_zero(std::to_underlying(v)); offset < ssize(flagValues))
214  return flagValues[offset];
215 
216  _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), qt_getEnumName(FlagT()));
217  Q_ASSERT(false);
218  return {};
219 }
220 
221 // Specialisations
222 
223 template<>
224 inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); }
225 
226 template <>
227 inline int fromJson(const QJsonValue& jv) { return jv.toInt(); }
228 
229 template <>
230 inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); }
231 
232 template <>
233 inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); }
234 
235 template <>
236 inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); }
237 
238 template <>
239 inline QString fromJson(const QJsonValue& jv) { return jv.toString(); }
240 
241 //! Use fromJson<QString> and then toLatin1()/toUtf8()/... to make QByteArray
242 //!
243 //! QJsonValue can only convert to QString and there's ambiguity whether
244 //! conversion to QByteArray should use (fast but very limited) toLatin1() or
245 //! (all encompassing and conforming to the JSON spec but slow) toUtf8().
246 template <>
247 inline QByteArray fromJson(const QJsonValue& jv) = delete;
248 
249 template <>
250 inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); }
251 
252 template <>
253 inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); }
254 
255 inline QJsonValue toJson(const QDateTime& val)
256 {
257  return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue();
258 }
259 template <>
260 inline QDateTime fromJson(const QJsonValue& jv)
261 {
262  return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC);
263 }
264 
265 inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); }
266 template <>
267 inline QDate fromJson(const QJsonValue& jv)
268 {
269  return fromJson<QDateTime>(jv).date();
270 }
271 
272 // Insulate QVariant and QUrl conversions into JsonConverter so that they don't
273 // interfere with toJson(const QJsonValue&) over QString, since both types are
274 // constructible from QString (even if QUrl requires explicit construction).
275 
276 template <>
277 struct JsonConverter<QUrl> {
278  static auto load(const QJsonValue& jv)
279  {
280  return QUrl(jv.toString());
281  }
282  static auto dump(const QUrl& url)
283  {
284  return url.toString(QUrl::FullyEncoded);
285  }
286 };
287 
288 template <>
289 struct QUOTIENT_API JsonConverter<QVariant> {
290  static QJsonValue dump(const QVariant& v);
291  static QVariant load(const QJsonValue& jv);
292 };
293 
294 template <typename... Ts>
295 inline QJsonValue toJson(const std::variant<Ts...>& v)
296 {
297  // std::visit requires all overloads to return the same type - and
298  // QJsonValue is a perfect candidate for that same type (assuming that
299  // variants never occur on the top level in Matrix API)
300  return std::visit(
301  [](const auto& value) { return QJsonValue { toJson(value) }; }, v);
302 }
303 
304 template <typename T>
305 struct JsonConverter<std::variant<QString, T>> {
306  static std::variant<QString, T> load(const QJsonValue& jv)
307  {
308  if (jv.isString())
309  return fromJson<QString>(jv);
310  return fromJson<T>(jv);
311  }
312 };
313 
314 template <typename T>
315 struct JsonConverter<std::optional<T>> {
316  static QJsonValue dump(const std::optional<T>& from)
317  {
318  return from.has_value() ? toJson(*from) : QJsonValue();
319  }
320  static std::optional<T> load(const QJsonValue& jv)
321  {
322  if (jv.isUndefined() || jv.isNull())
323  return std::nullopt;
324  return fromJson<T>(jv);
325  }
326 };
327 
328 template <typename ContT>
329 struct JsonArrayConverter {
330  static auto dump(const ContT& vals)
331  {
332  QJsonArray ja;
333  for (const auto& v : vals)
334  ja.push_back(toJson(v));
335  return ja;
336  }
337  static auto load(const QJsonArray& ja)
338  {
339  ContT vals;
340  vals.reserve(static_cast<typename ContT::size_type>(ja.size()));
341  // NB: Make sure fromJson<> gets QJsonValue (not QJsonValue*Ref)
342  // to avoid it falling back to the generic implementation that treats
343  // everything as an object. See also the message of commit 20f01303b
344  // that introduced these lines.
345  for (const auto& v : ja)
346  vals.push_back(fromJson<typename ContT::value_type, QJsonValue>(v));
347  return vals;
348  }
349  static auto load(const QJsonValue& jv) { return load(jv.toArray()); }
350  static auto load(const QJsonDocument& jd) { return load(jd.array()); }
351 };
352 
353 template <typename T>
354 struct JsonConverter<std::vector<T>>
355  : public JsonArrayConverter<std::vector<T>> {};
356 
357 template <typename T, size_t N>
358 struct JsonConverter<std::array<T, N>> {
359  // The size of std::array is known at compile-time and those arrays
360  // are usually short. The common conversion logic therefore is to expand
361  // the passed source array into a pack of values converted with to/fromJson
362  // and then construct the target array list-initialised with that pack.
363  // For load(), this implies that if QJsonArray is not of the right size,
364  // the resulting std::array will not have extra values or will have empty
365  // values at the end - silently.
366  static constexpr std::make_index_sequence<N> Indices{};
367  template <typename TargetT, size_t... I>
368  static auto staticTransform(const auto& source, std::index_sequence<I...>,
369  auto unaryFn)
370  {
371  return TargetT { unaryFn(source[I])... };
372  }
373  static auto dump(const std::array<T, N> a)
374  {
375  return staticTransform<QJsonArray>(a, Indices, [](const T& v) {
376  return toJson(v);
377  });
378  }
379  static auto load(const QJsonArray& ja)
380  {
381  return staticTransform<std::array<T, N>>(ja, Indices,
382  fromJson<T, QJsonValue>);
383  }
384 };
385 
386 template <typename T>
387 struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {};
388 
389 template <>
390 struct JsonConverter<QStringList> : public JsonArrayConverter<QStringList> {
391  static auto dump(const QStringList& sl)
392  {
393  return QJsonArray::fromStringList(sl);
394  }
395 };
396 
397 template <>
398 struct JsonObjectConverter<QSet<QString>> {
399  static void dumpTo(QJsonObject& json, const QSet<QString>& s)
400  {
401  for (const auto& e : s)
402  json.insert(e, QJsonObject {});
403  }
404  static void fillFrom(const QJsonObject& json, QSet<QString>& s)
405  {
406  s.reserve(s.size() + json.size());
407  for (auto it = json.begin(); it != json.end(); ++it)
408  s.insert(it.key());
409  }
410 };
411 
412 template <typename HashMapT>
413 struct HashMapFromJson {
414  static void dumpTo(QJsonObject& json, const HashMapT& hashMap)
415  {
416  for (auto it = hashMap.begin(); it != hashMap.end(); ++it)
417  json.insert(it.key(), toJson(it.value()));
418  }
419  static void fillFrom(const QJsonObject& jo, HashMapT& h)
420  {
421  h.reserve(h.size() + jo.size());
422  // NB: coercing the passed value to QJsonValue below is for
423  // the same reason as in JsonArrayConverter
424  for (auto it = jo.begin(); it != jo.end(); ++it)
425  h[it.key()] = fromJson<typename HashMapT::mapped_type, QJsonValue>(
426  it.value());
427  }
428 };
429 
430 template <typename T, typename HashT>
431 struct JsonObjectConverter<std::unordered_map<QString, T, HashT>>
432  : public HashMapFromJson<std::unordered_map<QString, T, HashT>> {};
433 
434 template <typename T>
435 struct JsonObjectConverter<QHash<QString, T>>
436  : public HashMapFromJson<QHash<QString, T>> {};
437 
438 QJsonObject QUOTIENT_API toJson(const QVariantHash& vh);
439 template <>
440 QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv);
441 
442 // Conditional insertion into a QJsonObject
443 
444 constexpr bool IfNotEmpty = false;
445 
446 namespace _impl {
447  template <typename ValT>
448  inline void addTo(QJsonObject& o, const QString& k, ValT&& v)
449  {
450  o.insert(k, toJson(std::forward<ValT>(v)));
451  }
452 
453  inline void addTo(QUrlQuery& q, const QString& k, auto v)
454  requires requires { QStringLiteral("%1").arg(v); }
455  {
456  q.addQueryItem(k, QStringLiteral("%1").arg(v));
457  }
458 
459  // OpenAPI is entirely JSON-based, which means representing bools as
460  // textual true/false, rather than 1/0.
461  inline void addTo(QUrlQuery& q, const QString& k, bool v)
462  {
463  q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false"));
464  }
465 
466  inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v)
467  {
468  q.addQueryItem(k, QString::fromLatin1(v.toEncoded()));
469  }
470 
471  inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals)
472  {
473  for (const auto& v : vals)
474  q.addQueryItem(k, v);
475  }
476 
477  template <typename ValT>
478  inline void addTo(QUrlQuery& q, const QString&, const QHash<QString, ValT>& fields)
479  {
480  for (const auto& [k, v] : fields.asKeyValueRange())
481  addTo(q, k, v);
482  }
483 
484  // This one is for types that don't have isEmpty() and for all types
485  // when Force is true
486  template <typename ValT, bool Force = true>
487  struct AddNode {
488  template <typename ForwardedT>
489  static void impl(auto& container, const QString& key, ForwardedT&& value)
490  {
491  addTo(container, key, std::forward<ForwardedT>(value));
492  }
493  };
494 
495  // This one is for types that have isEmpty() when Force is false
496  template <typename ValT>
497  requires requires(ValT v) { v.isEmpty(); }
498  struct AddNode<ValT, IfNotEmpty> {
499  template <typename ForwardedT>
500  static void impl(auto& container, const QString& key, ForwardedT&& value)
501  {
502  if (!value.isEmpty())
503  addTo(container, key, std::forward<ForwardedT>(value));
504  }
505  };
506 
507  // This one unfolds optionals (also only when IfNotEmpty is requested)
508  template <typename ValT>
509  struct AddNode<std::optional<ValT>, IfNotEmpty> {
510  static void impl(auto& container, const QString& key, const auto& optValue)
511  {
512  if (optValue)
513  addTo(container, key, *optValue);
514  }
515  };
516 } // namespace _impl
517 
518 /*! Add a key-value pair to QJsonObject or QUrlQuery
519  *
520  * Adds a key-value pair(s) specified by \p key and \p value to
521  * \p container, optionally (in case IfNotEmpty is passed for the first
522  * template parameter) taking into account the value "emptiness".
523  * With IfNotEmpty, \p value is NOT added to the container if and only if:
524  * - it has a method `isEmpty()` and `value.isEmpty() == true`, or
525  * - it's an optional that has no value (`nullopt`).
526  *
527  * If \p container is a QUrlQuery, an attempt to fit \p value into it is
528  * made as follows:
529  * - if \p value is a QJsonObject, \p key is ignored and pairs from \p value
530  * are copied to \p container, assuming that the value in each pair
531  * is a string;
532  * - if \p value is a QStringList, it is "exploded" into a list of key-value
533  * pairs with key equal to \p key and value taken from each list item;
534  * - if \p value is a bool, its OpenAPI (i.e. JSON) representation is added
535  * to the query (`true` or `false`, respectively).
536  *
537  * \tparam Force add the pair even if the value is empty. This is true
538  * by default; passing IfNotEmpty or false for this parameter
539  * enables emptiness checks as described above
540  */
541 template <bool Force = true, typename ContT, typename ValT>
542 inline void addParam(ContT& container, const QString& key, ValT&& value)
543 {
544  _impl::AddNode<std::decay_t<ValT>, Force>::impl(container, key,
545  std::forward<ValT>(value));
546 }
547 
548 // This is a facility function to convert camelCase method/variable names
549 // used throughout Quotient to snake_case JSON keys - see usage in
550 // single_key_value.h and event.h (QUO_CONTENT_GETTER macro).
551 inline auto toSnakeCase(QLatin1String s)
552 {
553  QString result { s };
554  for (auto it = result.begin(); it != result.end(); ++it)
555  if (it->isUpper()) {
556  const auto offset = static_cast<int>(it - result.begin());
557  result.insert(offset, u'_'); // NB: invalidates iterators
558  it = result.begin() + offset + 1;
559  *it = it->toLower();
560  }
561  return result;
562 }
563 } // namespace Quotient