diff options
Diffstat (limited to 'lib/converters.h')
-rw-r--r-- | lib/converters.h | 408 |
1 files changed, 280 insertions, 128 deletions
diff --git a/lib/converters.h b/lib/converters.h index 543e9496..0fb36320 100644 --- a/lib/converters.h +++ b/lib/converters.h @@ -1,23 +1,9 @@ -/****************************************************************************** - * Copyright (C) 2017 Kitsune Ral <kitsune-ral@users.sf.net> - * - * This library is free software; you can redistribute it and/or - * modify it under the terms of the GNU Lesser General Public - * License as published by the Free Software Foundation; either - * version 2.1 of the License, or (at your option) any later version. - * - * This library is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU - * Lesser General Public License for more details. - * - * You should have received a copy of the GNU Lesser General Public - * License along with this library; if not, write to the Free Software - * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA - */ +// SPDX-FileCopyrightText: 2017 Kitsune Ral <kitsune-ral@users.sf.net> +// SPDX-License-Identifier: LGPL-2.1-or-later #pragma once +#include "omittable.h" #include "util.h" #include <QtCore/QDate> @@ -28,163 +14,312 @@ #include <QtCore/QUrlQuery> #include <QtCore/QVector> +#include <type_traits> #include <vector> +#include <variant> class QVariant; namespace Quotient { template <typename T> struct JsonObjectConverter { - static void dumpTo(QJsonObject& jo, const T& pod) { jo = pod.toJson(); } - static void fillFrom(const QJsonObject& jo, T& pod) { pod = T(jo); } + // To be implemented in specialisations + static void dumpTo(QJsonObject&, const T&) = delete; + static void fillFrom(const QJsonObject&, T&) = delete; +}; + +template <typename PodT, typename JsonT> +PodT fromJson(const JsonT&); + +template <typename T> +struct JsonObjectUnpacker { + // By default, revert to fromJson() so that one could provide a single + // fromJson<T, QJsonObject> specialisation instead of specialising + // the entire JsonConverter; if a different type of JSON value is needed + // (e.g., an array), specialising JsonConverter is inevitable + static T load(const QJsonValue& jv) { return fromJson<T>(jv.toObject()); } + static T load(const QJsonDocument& jd) { return fromJson<T>(jd.object()); } }; +//! \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 <typename T> -struct JsonConverter { - static QJsonObject dump(const T& pod) +struct JsonConverter : JsonObjectUnpacker<T> { + // 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 auto dump(const T& data) { - QJsonObject jo; - JsonObjectConverter<T>::dumpTo(jo, pod); - return jo; + if constexpr (requires() { data.toJson(); }) + return data.toJson(); + else { + QJsonObject jo; + JsonObjectConverter<T>::dumpTo(jo, data); + return jo; + } } - static T doLoad(const QJsonObject& jo) + + using JsonObjectUnpacker<T>::load; + static T load(const QJsonObject& jo) { - T pod; - JsonObjectConverter<T>::fillFrom(jo, pod); - return pod; + // 'else' below are required to suppress code generation for unused + // branches - 'return' is not enough + if constexpr (std::is_same_v<T, QJsonObject>) + return jo; + else if constexpr (std::is_constructible_v<T, QJsonObject>) + return T(jo); + else { + T pod; + JsonObjectConverter<T>::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 <typename T> inline auto toJson(const T& pod) +// -> can return anything from which QJsonValue or, in some cases, QJsonDocument +// is constructible { - return JsonConverter<T>::dump(pod); + if constexpr (std::is_constructible_v<QJsonValue, T>) + return pod; // No-op if QJsonValue can be directly constructed + else + return JsonConverter<T>::dump(pod); } -inline auto toJson(const QJsonObject& jo) { return jo; } - template <typename T> inline void fillJson(QJsonObject& json, const T& data) { JsonObjectConverter<T>::dumpTo(json, data); } -template <typename T> -inline T fromJson(const QJsonValue& jv) +template <typename PodT, typename JsonT> +inline PodT fromJson(const JsonT& json) +{ + // JsonT here can be whatever the respective JsonConverter specialisation + // accepts but by default it's QJsonValue, QJsonDocument, or QJsonObject + return JsonConverter<PodT>::load(json); +} + +// Convenience fromJson() overload that deduces PodT instead of requiring +// the coder to explicitly type it. It still enforces the +// overwrite-everything semantics of fromJson(), unlike fillFromJson() + +template <typename JsonT, typename PodT> +inline void fromJson(const JsonT& json, PodT& pod) { - return JsonConverter<T>::load(jv); + pod = fromJson<PodT>(json); } template <typename T> -inline T fromJson(const QJsonDocument& jd) +inline void fillFromJson(const QJsonValue& jv, T& pod) { - return JsonConverter<T>::load(jd); + if constexpr (requires() { JsonObjectConverter<T>::fillFrom({}, pod); }) { + JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); + return; + } else if (!jv.isUndefined()) + pod = fromJson<T>(jv); } -// 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() +namespace _impl { + void warnUnknownEnumValue(const QString& stringValue, + const char* enumTypeName); + void reportEnumOutOfBounds(uint32_t v, const char* enumTypeName); +} -template <typename T> -inline void fromJson(const QJsonValue& jv, T& pod) +//! \brief Facility string-to-enum converter +//! +//! This is to simplify enum loading from JSON - just specialise +//! Quotient::fromJson() and call this function from it, passing (aside from +//! the JSON value for the enum - that must be a string, not an int) any +//! iterable container of string'y values (const char*, QLatin1String, etc.) +//! matching respective enum values, 0-based. +//! \sa enumToJsonString +template <typename EnumT, typename EnumStringValuesT> +EnumT enumFromJsonString(const QString& s, const EnumStringValuesT& enumValues, + EnumT defaultValue) { - pod = jv.isUndefined() ? T() : fromJson<T>(jv); + static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>); + if (const auto it = std::find(cbegin(enumValues), cend(enumValues), s); + it != cend(enumValues)) + return EnumT(it - cbegin(enumValues)); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(EnumT())); + return defaultValue; } -template <typename T> -inline void fromJson(const QJsonDocument& jd, T& pod) +//! \brief Facility enum-to-string converter +//! +//! This does the same as enumFromJsonString, the other way around. +//! \note The source enumeration must not have gaps in values, or \p enumValues +//! has to match those gaps (i.e., if the source enumeration is defined +//! as <tt>{ Value1 = 1, Value2 = 3, Value3 = 5 }</tt> then \p enumValues +//! should be defined as <tt>{ "", "Value1", "", "Value2", "", "Value3" +//! }</tt> (mind the gap at value 0, in particular). +//! \sa enumFromJsonString +template <typename EnumT, typename EnumStringValuesT> +QString enumToJsonString(EnumT v, const EnumStringValuesT& enumValues) { - pod = fromJson<T>(jd); + static_assert(std::is_unsigned_v<std::underlying_type_t<EnumT>>); + if (v < size(enumValues)) + return enumValues[v]; + + _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), + qt_getEnumName(EnumT())); + Q_ASSERT(false); + return {}; } -template <typename T> -inline void fillFromJson(const QJsonValue& jv, T& pod) +//! \brief Facility converter for flags +//! +//! This is very similar to enumFromJsonString, except that the target +//! enumeration is assumed to be of a 'flag' kind - i.e. its values must be +//! a power-of-two sequence starting from 1, without gaps, so exactly 1,2,4,8,16 +//! and so on. +//! \note Unlike enumFromJsonString, the values start from 1 and not from 0, +//! with 0 being used for an invalid value by default. +//! \note This function does not support flag combinations. +//! \sa QUO_DECLARE_FLAGS, QUO_DECLARE_FLAGS_NS +template <typename FlagT, typename FlagStringValuesT> +FlagT flagFromJsonString(const QString& s, const FlagStringValuesT& flagValues, + FlagT defaultValue = FlagT(0U)) { - if (jv.isObject()) - JsonObjectConverter<T>::fillFrom(jv.toObject(), pod); - else if (!jv.isUndefined()) - pod = fromJson<T>(jv); + // Enums based on signed integers don't make much sense for flag types + static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>); + if (const auto it = std::find(cbegin(flagValues), cend(flagValues), s); + it != cend(flagValues)) + return FlagT(1U << (it - cbegin(flagValues))); + + if (!s.isEmpty()) + _impl::warnUnknownEnumValue(s, qt_getEnumName(FlagT())); + return defaultValue; } -// JsonConverter<> specialisations +template <typename FlagT, typename FlagStringValuesT> +QString flagToJsonString(FlagT v, const FlagStringValuesT& flagValues) +{ + static_assert(std::is_unsigned_v<std::underlying_type_t<FlagT>>); + if (const auto offset = + qCountTrailingZeroBits(std::underlying_type_t<FlagT>(v)); + offset < size(flagValues)) // + { + return flagValues[offset]; + } -template <typename T> -struct TrivialJsonDumper { - // Works for: QJsonValue (and all things it can consume), - // QJsonObject, QJsonArray - static auto dump(const T& val) { return val; } -}; + _impl::reportEnumOutOfBounds(static_cast<uint32_t>(v), + qt_getEnumName(FlagT())); + Q_ASSERT(false); + return {}; +} + +// Specialisations + +template<> +inline bool fromJson(const QJsonValue& jv) { return jv.toBool(); } template <> -struct JsonConverter<bool> : public TrivialJsonDumper<bool> { - static auto load(const QJsonValue& jv) { return jv.toBool(); } -}; +inline int fromJson(const QJsonValue& jv) { return jv.toInt(); } template <> -struct JsonConverter<int> : public TrivialJsonDumper<int> { - static auto load(const QJsonValue& jv) { return jv.toInt(); } -}; +inline double fromJson(const QJsonValue& jv) { return jv.toDouble(); } template <> -struct JsonConverter<double> : public TrivialJsonDumper<double> { - static auto load(const QJsonValue& jv) { return jv.toDouble(); } -}; +inline float fromJson(const QJsonValue& jv) { return float(jv.toDouble()); } template <> -struct JsonConverter<float> : public TrivialJsonDumper<float> { - static auto load(const QJsonValue& jv) { return float(jv.toDouble()); } -}; +inline qint64 fromJson(const QJsonValue& jv) { return qint64(jv.toDouble()); } template <> -struct JsonConverter<qint64> : public TrivialJsonDumper<qint64> { - static auto load(const QJsonValue& jv) { return qint64(jv.toDouble()); } -}; +inline QString fromJson(const QJsonValue& jv) { return jv.toString(); } +//! Use fromJson<QString> and then 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 <> -struct JsonConverter<QString> : public TrivialJsonDumper<QString> { - static auto load(const QJsonValue& jv) { return jv.toString(); } -}; +inline QByteArray fromJson(const QJsonValue& jv) = delete; template <> -struct JsonConverter<QDateTime> { - static auto dump(const QDateTime& val) = delete; // not provided yet - static auto load(const QJsonValue& jv) - { - return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); - } -}; +inline QJsonArray fromJson(const QJsonValue& jv) { return jv.toArray(); } template <> -struct JsonConverter<QDate> { - static auto dump(const QDate& val) = delete; // not provided yet - static auto load(const QJsonValue& jv) - { - return fromJson<QDateTime>(jv).date(); - } -}; +inline QJsonArray fromJson(const QJsonDocument& jd) { return jd.array(); } +inline QJsonValue toJson(const QDateTime& val) +{ + return val.isValid() ? val.toMSecsSinceEpoch() : QJsonValue(); +} template <> -struct JsonConverter<QJsonArray> : public TrivialJsonDumper<QJsonArray> { - static auto load(const QJsonValue& jv) { return jv.toArray(); } -}; +inline QDateTime fromJson(const QJsonValue& jv) +{ + return QDateTime::fromMSecsSinceEpoch(fromJson<qint64>(jv), Qt::UTC); +} +inline QJsonValue toJson(const QDate& val) { return toJson(val.startOfDay()); } template <> -struct JsonConverter<QByteArray> { - static QString dump(const QByteArray& ba) { return ba.constData(); } +inline QDate fromJson(const QJsonValue& jv) +{ + return fromJson<QDateTime>(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<QUrl> { static auto load(const QJsonValue& jv) { - return fromJson<QString>(jv).toLatin1(); + return QUrl(jv.toString()); + } + static auto dump(const QUrl& url) + { + return url.toString(QUrl::FullyEncoded); } }; template <> -struct JsonConverter<QVariant> { +struct QUOTIENT_API JsonConverter<QVariant> { static QJsonValue dump(const QVariant& v); static QVariant load(const QJsonValue& jv); }; +template <typename... Ts> +inline QJsonValue toJson(const std::variant<Ts...>& 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 <typename T> +struct JsonConverter<std::variant<QString, T>> { + static std::variant<QString, T> load(const QJsonValue& jv) + { + if (jv.isString()) + return fromJson<QString>(jv); + return fromJson<T>(jv); + } +}; + template <typename T> struct JsonConverter<Omittable<T>> { static QJsonValue dump(const Omittable<T>& from) @@ -201,23 +336,23 @@ struct JsonConverter<Omittable<T>> { template <typename VectorT, typename T = typename VectorT::value_type> struct JsonArrayConverter { - static void dumpTo(QJsonArray& ar, const VectorT& vals) - { - for (const auto& v : vals) - ar.push_back(toJson(v)); - } static auto dump(const VectorT& vals) { QJsonArray ja; - dumpTo(ja, vals); + 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<T>(i)); + // NB: Make sure to pass QJsonValue to fromJson<> so that it could + // hit the correct overload and not fall back to the generic fromJson + // that treats everything as an object. See also the explanation in + // the commit introducing these lines. + for (const QJsonValue v : ja) + vect.push_back(fromJson<T>(v)); return vect; } static auto load(const QJsonValue& jv) { return load(jv.toArray()); } @@ -228,14 +363,16 @@ template <typename T> struct JsonConverter<std::vector<T>> : public JsonArrayConverter<std::vector<T>> {}; +#if QT_VERSION_MAJOR < 6 // QVector is an alias of QList in Qt6 but not in Qt 5 template <typename T> struct JsonConverter<QVector<T>> : public JsonArrayConverter<QVector<T>> {}; +#endif template <typename T> struct JsonConverter<QList<T>> : public JsonArrayConverter<QList<T>> {}; template <> -struct JsonConverter<QStringList> : public JsonConverter<QList<QString>> { +struct JsonConverter<QStringList> : public JsonArrayConverter<QStringList> { static auto dump(const QStringList& sl) { return QJsonArray::fromStringList(sl); @@ -247,14 +384,13 @@ struct JsonObjectConverter<QSet<QString>> { static void dumpTo(QJsonObject& json, const QSet<QString>& s) { for (const auto& e : s) - json.insert(toJson(e), QJsonObject {}); + json.insert(e, QJsonObject {}); } - static auto fillFrom(const QJsonObject& json, QSet<QString>& s) + static void fillFrom(const QJsonObject& json, QSet<QString>& s) { s.reserve(s.size() + json.size()); for (auto it = json.begin(); it != json.end(); ++it) s.insert(it.key()); - return s; } }; @@ -267,9 +403,12 @@ struct HashMapFromJson { } static void fillFrom(const QJsonObject& jo, HashMapT& h) { - h.reserve(jo.size()); + h.reserve(h.size() + jo.size()); + // NB: the QJsonValue cast below is for the same reason as in + // JsonArrayConverter for (auto it = jo.begin(); it != jo.end(); ++it) - h[it.key()] = fromJson<typename HashMapT::mapped_type>(it.value()); + h[it.key()] = fromJson<typename HashMapT::mapped_type>( + QJsonValue(it.value())); } }; @@ -281,14 +420,14 @@ template <typename T> struct JsonObjectConverter<QHash<QString, T>> : public HashMapFromJson<QHash<QString, T>> {}; +QJsonObject QUOTIENT_API toJson(const QVariantHash& vh); template <> -struct JsonConverter<QVariantHash> { - static QJsonObject dump(const QVariantHash& vh); - static QVariantHash load(const QJsonValue& jv); -}; +QVariantHash QUOTIENT_API fromJson(const QJsonValue& jv); // Conditional insertion into a QJsonObject +constexpr bool IfNotEmpty = false; + namespace _impl { template <typename ValT> inline void addTo(QJsonObject& o, const QString& k, ValT&& v) @@ -309,16 +448,15 @@ namespace _impl { q.addQueryItem(k, v ? QStringLiteral("true") : QStringLiteral("false")); } - inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) + inline void addTo(QUrlQuery& q, const QString& k, const QUrl& v) { - for (const auto& v : vals) - q.addQueryItem(k, v); + q.addQueryItem(k, v.toEncoded()); } - inline void addTo(QUrlQuery& q, const QString&, const QJsonObject& vals) + inline void addTo(QUrlQuery& q, const QString& k, const QStringList& vals) { - for (auto it = vals.begin(); it != vals.end(); ++it) - q.addQueryItem(it.key(), it.value().toString()); + for (const auto& v : vals) + q.addQueryItem(k, v); } // This one is for types that don't have isEmpty() and for all types @@ -335,7 +473,7 @@ namespace _impl { // This one is for types that have isEmpty() when Force is false template <typename ValT> - struct AddNode<ValT, false, decltype(std::declval<ValT>().isEmpty())> { + struct AddNode<ValT, IfNotEmpty, decltype(std::declval<ValT>().isEmpty())> { template <typename ContT, typename ForwardedT> static void impl(ContT& container, const QString& key, ForwardedT&& value) @@ -345,9 +483,9 @@ namespace _impl { } }; - // This one unfolds Omittable<> (also only when Force is false) + // This one unfolds Omittable<> (also only when IfNotEmpty is requested) template <typename ValT> - struct AddNode<Omittable<ValT>, false> { + struct AddNode<Omittable<ValT>, IfNotEmpty> { template <typename ContT, typename OmittableT> static void impl(ContT& container, const QString& key, const OmittableT& value) @@ -358,8 +496,6 @@ namespace _impl { }; } // namespace _impl -static constexpr bool IfNotEmpty = false; - /*! Add a key-value pair to QJsonObject or QUrlQuery * * Adds a key-value pair(s) specified by \p key and \p value to @@ -389,4 +525,20 @@ inline void addParam(ContT& container, const QString& key, ValT&& value) _impl::AddNode<std::decay_t<ValT>, Force>::impl(container, key, std::forward<ValT>(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<int>(it - result.begin()); + result.insert(offset, '_'); // NB: invalidates iterators + it = result.begin() + offset + 1; + *it = it->toLower(); + } + return result; +} } // namespace Quotient |