// SPDX-FileCopyrightText: 2017 Kitsune Ral // SPDX-License-Identifier: LGPL-2.1-or-later #pragma once #include "omittable.h" #include "util.h" #include #include // Includes #include #include #include #include #include #include #include #include class QVariant; namespace Quotient { template struct JsonObjectConverter { // To be implemented in specialisations static void dumpTo(QJsonObject&, const T&) = delete; static void fillFrom(const QJsonObject&, T&) = delete; }; namespace _impl { template struct JsonExporter { static QJsonObject dump(const T& data) { QJsonObject jo; JsonObjectConverter::dumpTo(jo, data); return jo; } }; template struct JsonExporter< T, std::enable_if_t>> { static auto dump(const T& data) { return data.toJson(); } }; } //! \brief The switchboard for extra conversion algorithms behind from/toJson //! //! This template is mainly intended for partial conversion specialisations //! since from/toJson are functions and cannot be partially specialised. //! Another case for JsonConverter is to insulate types that can be constructed //! from basic types - namely, QVariant and QUrl can be directly constructed //! from QString and having an overload or specialisation for those leads to //! ambiguity between these and QJsonValue. For trivial (converting //! QJsonObject/QJsonValue) and most simple cases such as primitive types or //! QString this class is not needed. //! //! Do NOT call the functions of this class directly unless you know what you're //! doing; and do not try to specialise basic things unless you're really sure //! that they are not supported and it's not feasible to support those by means //! of overloading toJson() and specialising fromJson(). template struct JsonConverter : _impl::JsonExporter { // Unfortunately, if constexpr doesn't work with dump() and T::toJson // because trying to check invocability of T::toJson hits a hard // (non-SFINAE) compilation error if the member is not there. Hence a bit // more verbose SFINAE construct in _impl::JsonExporter. static T doLoad(const QJsonObject& jo) { // 'else' below are required to suppress code generation for unused // branches - 'return' is not enough if constexpr (std::is_same_v) return jo; else if constexpr (std::is_constructible_v) return T(jo); else { T pod; JsonObjectConverter::fillFrom(jo, pod); return pod; } } static T load(const QJsonValue& jv) { return doLoad(jv.toObject()); } static T load(const QJsonDocument& jd) { return doLoad(jd.object()); } }; template >> inline auto toJson(const T& pod) // -> can return anything from which QJsonValue or, in some cases, QJsonDocument // is constructible { return JsonConverter::dump(pod); } inline auto toJson(const QJsonObject& jo) { return jo; } inline auto toJson(const QJsonValue& jv) { return jv; } template inline void fillJson(QJsonObject& json, const T& data) { JsonObjectConverter::dumpTo(json, data); } template inline T fromJson(const QJsonValue& jv) { return JsonConverter::load(jv); } template<> inline QJsonValue fromJson(const QJsonValue& jv) { return jv; } template inline T fromJson(const QJsonDocument& jd) { return JsonConverter::load(jd); } // Convenience fromJson() overloads that deduce T instead of requiring // the coder to explicitly type it. They still enforce the // overwrite-everything semantics of fromJson(), unlike fillFromJson() template inline void fromJson(const QJsonValue& jv, T& pod) { pod = jv.isUndefined() ? T() : fromJson(jv); } template inline void fromJson(const QJsonDocument& jd, T& pod) { pod = fromJson(jd); } template inline void fillFromJson(const QJsonValue& jv, T& pod) { if (jv.isObject()) JsonObjectConverter::fillFrom(jv.toObject(), pod); else if (!jv.isUndefined()) pod = fromJson(jv); } // JsonConverter<> specialisations template<> inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); } template <> inline int fromJson(const QJsonValue& jv) { return jv.toInt(); } template <> inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); } template <> inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); } template <> inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } template <> inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } //! Use fromJson and use toLatin1()/toUtf8()/... to make QByteArray //! //! QJsonValue can only convert to QString and there's ambiguity whether //! conversion to QByteArray should use (fast but very limited) toLatin1() or //! (all encompassing and conforming to the JSON spec but slow) toUtf8(). template <> inline QByteArray fromJson(const QJsonValue& jv) = delete; template <> inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); } template <> inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); } inline QJsonValue toJson(const QDateTime& val) { return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); } template <> inline QDateTime fromJson(const QJsonValue& jv) { return QDateTime::fromMSecsSinceEpoch(fromJson(jv), Qt::UTC); } inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); } template <> inline QDate fromJson(const QJsonValue& jv) { return fromJson(jv).date(); } // Insulate QVariant and QUrl conversions into JsonConverter so that they don't // interfere with toJson(const QJsonValue&) over QString, since both types are // constructible from QString (even if QUrl requires explicit construction). template <> struct JsonConverter { static auto load(const QJsonValue& jv) { return QUrl(jv.toString()); } static auto dump(const QUrl& url) { return url.toString(QUrl::FullyEncoded); } }; template <> struct QUOTIENT_API JsonConverter { static QJsonValue dump(const QVariant& v); static QVariant load(const QJsonValue& jv); }; template inline QJsonValue toJson(const std::variant& v) { // std::visit requires all overloads to return the same type - and // QJsonValue is a perfect candidate for that same type (assuming that // variants never occur on the top level in Matrix API) return std::visit( [](const auto& value) { return QJsonValue { toJson(value) }; }, v); } template struct JsonConverter> { static std::variant load(const QJsonValue& jv) { if (jv.isString()) return fromJson(jv); return fromJson(jv); } }; template struct JsonConverter> { static QJsonValue dump(const Omittable& from) { return from.has_value() ? toJson(from.value()) : QJsonValue(); } static Omittable load(const QJsonValue& jv) { if (jv.isUndefined()) return none; return fromJson(jv); } }; template struct JsonArrayConverter { static auto dump(const VectorT& vals) { QJsonArray ja; for (const auto& v : vals) ja.push_back(toJson(v)); return ja; } static auto load(const QJsonArray& ja) { VectorT vect; vect.reserve(typename VectorT::size_type(ja.size())); for (const auto& i : ja) vect.push_back(fromJson(i)); return vect; } static auto load(const QJsonValue& jv) { return load(jv.toArray()); } static auto load(const QJsonDocument& jd) { return load(jd.array()); } }; template struct JsonConverter> : public JsonArrayConverter> {}; #if QT_VERSION_MAJOR < 6 // QVector is an alias of QList in Qt6 but not in Qt 5 template struct JsonConverter> : public JsonArrayConverter> {}; #endif template struct JsonConverter> : public JsonArrayConverter> {}; template <> struct JsonConverter : public JsonArrayConverter { static auto dump(const QStringList& sl) { return QJsonArray::fromStringList(sl); } }; template <> struct JsonObjectConverter> { static void dumpTo(QJsonObject& json, const QSet& s) { for (const auto& e : s) json.insert(e, QJsonObject {}); } static void fillFrom(const QJsonObject& json, QSet& s) { s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); } }; template struct HashMapFromJson { static void dumpTo(QJsonObject& json, const HashMapT& hashMap) { for (auto it = hashMap.begin(); it != hashMap.end(); ++it) json.insert(it.key(), toJson(it.value())); } static void fillFrom(const QJsonObject& jo, HashMapT& h) { h.reserve(h.size() + jo.size()); for (auto it = jo.begin(); it != jo.end(); ++it) h[it.key()] = fromJson(it.value()); } }; template struct JsonObjectConverter> : public HashMapFromJson> {}; template struct JsonObjectConverter> : public HashMapFromJson> {}; QJsonObject QUOTIENT_API toJson(const QVariantHash& vh); template <> QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject constexpr bool IfNotEmpty = false; namespace _impl { template inline void addTo(QJsonObject& o, const QString& k, ValT&& v) { o.insert(k, toJson(v)); } template inline void addTo(QUrlQuery& q, const QString& k, ValT&& v) { q.addQueryItem(k, QStringLiteral("%1").arg(v)); } // OpenAPI is entirely JSON-based, which means representing bools as // textual true/false, rather than 1/0. inline void addTo(QUrlQuery& q, const QString& k, bool v) { q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false")); } inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v) { q.addQueryItem(k, v.toEncoded()); } inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) { for (const auto& v : vals) q.addQueryItem(k, v); } // This one is for types that don't have isEmpty() and for all types // when Force is true template struct AddNode { template static void impl(ContT& container, const QString& key, ForwardedT&& value) { addTo(container, key, std::forward(value)); } }; // This one is for types that have isEmpty() when Force is false template struct AddNode().isEmpty())> { template static void impl(ContT& container, const QString& key, ForwardedT&& value) { if (!value.isEmpty()) addTo(container, key, std::forward(value)); } }; // This one unfolds Omittable<> (also only when IfNotEmpty is requested) template struct AddNode, IfNotEmpty> { template static void impl(ContT& container, const QString& key, const OmittableT& value) { if (value) addTo(container, key, *value); } }; } // namespace _impl /*! Add a key-value pair to QJsonObject or QUrlQuery * * Adds a key-value pair(s) specified by \p key and \p value to * \p container, optionally (in case IfNotEmpty is passed for the first * template parameter) taking into account the value "emptiness". * With IfNotEmpty, \p value is NOT added to the container if and only if: * - it has a method `isEmpty()` and `value.isEmpty() == true`, or * - it's an `Omittable<>` and `value.omitted() == true`. * * If \p container is a QUrlQuery, an attempt to fit \p value into it is * made as follows: * - if \p value is a QJsonObject, \p key is ignored and pairs from \p value * are copied to \p container, assuming that the value in each pair * is a string; * - if \p value is a QStringList, it is "exploded" into a list of key-value * pairs with key equal to \p key and value taken from each list item; * - if \p value is a bool, its OpenAPI (i.e. JSON) representation is added * to the query (`true` or `false`, respectively). * * \tparam Force add the pair even if the value is empty. This is true * by default; passing IfNotEmpty or false for this parameter * enables emptiness checks as described above */ template inline void addParam(ContT& container, const QString& key, ValT&& value) { _impl::AddNode, Force>::impl(container, key, std::forward(value)); } // This is a facility function to convert camelCase method/variable names // used throughout Quotient to snake_case JSON keys - see usage in // single_key_value.h and event.h (DEFINE_CONTENT_GETTER macro). inline auto toSnakeCase(QLatin1String s) { QString result { s }; for (auto it = result.begin(); it != result.end(); ++it) if (it->isUpper()) { const auto offset = static_cast(it - result.begin()); result.insert(offset, '_'); // NB: invalidates iterators it = result.begin() + offset + 1; *it = it->toLower(); } return result; } } // namespace Quotient