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 #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 
25 class QVariant;
26 
27 namespace Quotient {
28 
29 inline 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 
36 inline 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 
43 template <typename T>
44 struct 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 
49 template <typename PodT, typename JsonT>
50 PodT fromJson(const JsonT&);
51 
52 template <typename T>
53 struct JsonObjectUnpacker {
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().
77 template <typename T>
78 struct JsonConverter : JsonObjectUnpacker<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 
107 template <typename T>
108 inline 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 
118 template <typename T>
119 inline void fillJson(QJsonObject& json, const T& data)
120 {
121  JsonObjectConverter<T>::dumpTo(json, data);
122 }
123 
124 template <typename PodT, typename JsonT>
125 inline 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 
136 template <typename JsonT, typename PodT>
137 inline void fromJson(const JsonT& json, PodT& pod)
138 {
139  pod = fromJson<PodT>(json);
140 }
141 
142 template <typename T>
143 inline 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 
152 namespace _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
164 template <typename EnumT, typename EnumStringValuesT>
165 inline 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
183 template <typename EnumT, typename EnumStringValuesT>
184 inline 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
205 template <typename FlagT, typename FlagStringValuesT>
206 inline 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 
216 template <typename FlagT, typename FlagStringValuesT>
217 inline 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 
230 template <>
231 inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); }
232 
233 template <>
234 inline int fromJson(const QJsonValue& jv) { return jv.toInt(); }
235 
236 template <>
237 inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); }
238 
239 template <>
240 inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); }
241 
242 template <>
243 inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); }
244 
245 template <>
246 inline 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().
253 template <>
254 inline QByteArray fromJson(const QJsonValue& jv) = delete;
255 
256 template <>
257 inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); }
258 
259 template <>
260 inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); }
261 
262 inline QJsonValue toJson(const QDateTime& val)
263 {
264  return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue();
265 }
266 template <>
267 inline 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 
277 inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); }
278 template <>
279 inline 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 
288 template <>
289 struct 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 
300 template <>
301 struct QUOTIENT_API JsonConverter<QVariant> {
302  static QJsonValue dump(const QVariant& v);
303  static QVariant load(const QJsonValue& jv);
304 };
305 
306 template <typename... Ts>
307 inline 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 
316 template <typename T>
317 struct 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 
326 template <typename T>
327 struct 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 
340 template <typename ContT>
341 struct JsonArrayConverter {
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 
365 template <typename T>
366 struct JsonConverter<std::vector<T>>
367  : public JsonArrayConverter<std::vector<T>> {};
368 
369 template <typename T, size_t N>
370 struct 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 
398 template <typename T>
399 struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {};
400 
401 template <>
402 struct JsonConverter<QStringList> : public JsonArrayConverter<QStringList> {
403  static auto dump(const QStringList& sl)
404  {
405  return QJsonArray::fromStringList(sl);
406  }
407 };
408 
409 template <>
410 struct JsonObjectConverter<QSet<QString>> {
411  static void dumpTo(QJsonObject& json, const QSet<QString>& s)
412  {
413  for (const auto& e : s)
414  json.insert(e, QJsonObject {});
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 
424 template <typename HashMapT>
425 struct HashMapFromJson {
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 
442 template <typename T, typename HashT>
443 struct JsonObjectConverter<std::unordered_map<QString, T, HashT>>
444  : public HashMapFromJson<std::unordered_map<QString, T, HashT>> {};
445 
446 template <typename T>
447 struct JsonObjectConverter<QHash<QString, T>>
448  : public HashMapFromJson<QHash<QString, T>> {};
449 
450 QJsonObject QUOTIENT_API toJson(const QVariantHash& vh);
451 template <>
452 QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv);
453 
454 // Conditional insertion into a QJsonObject
455 
456 constexpr bool IfNotEmpty = false;
457 
458 namespace _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  */
554 template <bool Force = true, typename ContT, typename KeyT, typename ValT>
555 inline 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).
564 inline 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