372 lines
12 KiB
C++
372 lines
12 KiB
C++
#include "codecs.hpp"
|
|
|
|
#include <qjsonarray.h>
|
|
#include <qjsonobject.h>
|
|
#include <qmetaobject.h>
|
|
|
|
#include <cmath>
|
|
|
|
#include "util/metaenum.hpp"
|
|
|
|
namespace ZShell::settings {
|
|
|
|
using Qt::StringLiterals::operator""_s;
|
|
|
|
namespace {
|
|
|
|
// We don't know the option here so it isn't set, it should be set by the caller
|
|
DecodeResult error(DiagnosticType::Type type, const QString& message) {
|
|
Diagnostic error;
|
|
error.type = type;
|
|
error.message = message;
|
|
return { .value = QVariant(), .error = error, .indexPath = {} };
|
|
}
|
|
|
|
DecodeResult mismatch(ExpectedType expected, const QJsonValue& value) {
|
|
return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} };
|
|
}
|
|
|
|
DecodeResult mismatch(const QList<ExpectedType>& expected, const QJsonValue& value) {
|
|
return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} };
|
|
}
|
|
|
|
template <typename Container> ValueCodec* makeListCodec(const QMetaType& type) {
|
|
const auto* elementCodec = ValueCodec::codecFor(QMetaType::fromType<typename Container::value_type>());
|
|
return elementCodec ? new ListCodec<Container>(type, elementCodec) : nullptr;
|
|
}
|
|
|
|
// Unions are keyed by their alternatives in order, since order decides which one wins
|
|
QString unionKey(const QList<QMetaType>& types) {
|
|
QStringList ids;
|
|
ids.reserve(types.size());
|
|
for (const auto& type : types)
|
|
ids << QString::number(type.id());
|
|
return ids.join(u","_s);
|
|
}
|
|
|
|
QList<ExpectedType> expectedTypesOf(const QList<const ValueCodec*>& alternatives) {
|
|
QList<ExpectedType> types;
|
|
types.reserve(alternatives.size());
|
|
for (const auto* codec : alternatives)
|
|
types << codec->expected();
|
|
return types;
|
|
}
|
|
|
|
using ListFactory = ValueCodec* (*)(const QMetaType&);
|
|
|
|
const QHash<int, ListFactory>& listFactories() {
|
|
static const QHash<int, ListFactory> k_factories{
|
|
{ QMetaType::fromType<QStringList>().id(), &makeListCodec<QStringList> },
|
|
{ QMetaType::fromType<QList<qreal>>().id(), &makeListCodec<QList<qreal>> },
|
|
};
|
|
return k_factories;
|
|
}
|
|
|
|
} // namespace
|
|
|
|
ValueCodec::ValueCodec(const QMetaType& type, ExpectedType expected)
|
|
: m_type(type)
|
|
, m_expected(expected) {}
|
|
|
|
QMetaType ValueCodec::type() const {
|
|
return m_type;
|
|
}
|
|
|
|
ExpectedType ValueCodec::expected() const {
|
|
return m_expected;
|
|
}
|
|
|
|
ValueCodec* ValueCodec::codecFor(const QMetaType& type) {
|
|
// Cache for codecs, keyed by type id
|
|
static QHash<int, ValueCodec*> s_registry;
|
|
|
|
// Cached lookup
|
|
if (const auto it = s_registry.constFind(type.id()); it != s_registry.constEnd())
|
|
return *it;
|
|
|
|
ValueCodec* codec = nullptr;
|
|
switch (type.id()) {
|
|
case QMetaType::Bool:
|
|
codec = new BoolCodec(type);
|
|
break;
|
|
case QMetaType::Int:
|
|
codec = new IntCodec(type);
|
|
break;
|
|
case QMetaType::Double:
|
|
codec = new RealCodec(type);
|
|
break;
|
|
case QMetaType::QString:
|
|
codec = new StringCodec(type);
|
|
break;
|
|
case QMetaType::QVariantList:
|
|
codec = new VariantListCodec(type);
|
|
break;
|
|
case QMetaType::QVariantMap:
|
|
codec = new VariantMapCodec(type);
|
|
break;
|
|
default:
|
|
if (const auto factory = listFactories().constFind(type.id()); factory != listFactories().constEnd())
|
|
codec = (*factory)(type);
|
|
else if (util::isSupportedEnum(type))
|
|
codec = new EnumCodec(type, util::metaEnumFor(type));
|
|
break;
|
|
}
|
|
|
|
// Cache codec
|
|
if (codec)
|
|
s_registry.insert(type.id(), codec);
|
|
|
|
return codec;
|
|
}
|
|
|
|
ValueCodec* ValueCodec::unionFor(const QList<QMetaType>& types) {
|
|
// Cache for union codecs, keyed by their alternatives
|
|
static QHash<QString, ValueCodec*> s_registry;
|
|
|
|
if (types.size() < 2) {
|
|
qCCritical(lcSettings, "A union needs at least two types, got %lld", types.size());
|
|
return nullptr;
|
|
}
|
|
|
|
const auto key = unionKey(types);
|
|
|
|
// Cached lookup
|
|
if (const auto it = s_registry.constFind(key); it != s_registry.constEnd())
|
|
return *it;
|
|
|
|
QList<const ValueCodec*> alternatives;
|
|
alternatives.reserve(types.size());
|
|
|
|
for (const auto& type : types) {
|
|
const auto* codec = codecFor(type);
|
|
if (!codec) {
|
|
qCCritical(lcSettings, "No codec found for type %s, cannot build union", type.name());
|
|
return nullptr;
|
|
}
|
|
alternatives << codec;
|
|
}
|
|
|
|
auto* const codec = new UnionCodec(alternatives);
|
|
s_registry.insert(key, codec);
|
|
|
|
return codec;
|
|
}
|
|
|
|
QJsonValue BoolCodec::encode(const QVariant& value) const {
|
|
return value.toBool();
|
|
}
|
|
|
|
DecodeResult BoolCodec::decode(const QJsonValue& value) const {
|
|
// 1 and "true" are not booleans
|
|
if (!value.isBool())
|
|
return mismatch(ExpectedType::Bool, value);
|
|
|
|
return { .value = value.toBool(), .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
QJsonValue IntCodec::encode(const QVariant& value) const {
|
|
return value.toInt();
|
|
}
|
|
|
|
DecodeResult IntCodec::decode(const QJsonValue& value) const {
|
|
if (!value.isDouble())
|
|
return mismatch(ExpectedType::Int, value);
|
|
|
|
const auto num = value.toDouble();
|
|
|
|
double integral;
|
|
if (std::fpclassify(std::modf(num, &integral)) != FP_ZERO)
|
|
return error(
|
|
DiagnosticType::InvalidValue, QStringLiteral("Expected an integer, got the real %1").arg(QString::number(num)));
|
|
|
|
constexpr auto k_min = std::numeric_limits<int>::min();
|
|
constexpr auto k_max = std::numeric_limits<int>::max();
|
|
if (num < static_cast<double>(k_min) || num > static_cast<double>(k_max)) {
|
|
const auto message = QStringLiteral("Integer %1 is out of range, expected between %2 and %3").arg(QString::number(num, 'f', 0), QString::number(k_min), QString::number(k_max));
|
|
return error(DiagnosticType::InvalidValue, message);
|
|
}
|
|
|
|
return { .value = static_cast<int>(num), .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
QJsonValue RealCodec::encode(const QVariant& value) const {
|
|
return value.toDouble();
|
|
}
|
|
|
|
DecodeResult RealCodec::decode(const QJsonValue& value) const {
|
|
if (!value.isDouble())
|
|
return mismatch(ExpectedType::Real, value);
|
|
|
|
return { .value = QVariant::fromValue<qreal>(value.toDouble()), .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
QJsonValue StringCodec::encode(const QVariant& value) const {
|
|
return value.toString();
|
|
}
|
|
|
|
DecodeResult StringCodec::decode(const QJsonValue& value) const {
|
|
if (!value.isString())
|
|
return mismatch(ExpectedType::String, value);
|
|
|
|
return { .value = value.toString(), .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
QJsonValue VariantListCodec::encode(const QVariant& value) const {
|
|
return QJsonArray::fromVariantList(value.toList());
|
|
}
|
|
|
|
DecodeResult VariantListCodec::decode(const QJsonValue& value) const {
|
|
if (!value.isArray())
|
|
return mismatch(ExpectedType::Array, value);
|
|
|
|
return { .value = value.toArray().toVariantList(), .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
QJsonValue VariantMapCodec::encode(const QVariant& value) const {
|
|
return QJsonObject::fromVariantMap(value.toMap());
|
|
}
|
|
|
|
DecodeResult VariantMapCodec::decode(const QJsonValue& value) const {
|
|
if (!value.isObject())
|
|
return mismatch(ExpectedType::Object, value);
|
|
|
|
return { .value = value.toObject().toVariantMap(), .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
EnumCodec::EnumCodec(const QMetaType& type, const QMetaEnum& metaEnum)
|
|
: ValueCodec(type, ExpectedType::String)
|
|
, m_metaEnum(metaEnum) {}
|
|
|
|
QJsonValue EnumCodec::encode(const QVariant& value) const {
|
|
const auto* key = util::enumKeyFor(m_metaEnum, value);
|
|
if (!key) {
|
|
qCWarning(
|
|
lcSettings, "Cannot encode value %lld of enum %s, no such enumerator", value.toLongLong(), m_type.name());
|
|
return {};
|
|
}
|
|
|
|
return QString::fromUtf8(key);
|
|
}
|
|
|
|
DecodeResult EnumCodec::decode(const QJsonValue& value) const {
|
|
if (!value.isString())
|
|
return mismatch(ExpectedType::String, value);
|
|
|
|
const auto key = value.toString();
|
|
|
|
for (int i = 0; i < m_metaEnum.keyCount(); ++i) {
|
|
if (QString::compare(QString::fromUtf8(m_metaEnum.key(i)), key, Qt::CaseInsensitive) != 0)
|
|
continue;
|
|
|
|
const auto raw = m_metaEnum.value(i);
|
|
QVariant decoded(m_type);
|
|
if (!QMetaType::convert(QMetaType::fromType<int>(), &raw, m_type, decoded.data())) {
|
|
qCCritical(lcSettings, "Failed to convert value %d to enum %s", raw, m_type.name());
|
|
return error(DiagnosticType::InvalidValue,
|
|
QStringLiteral("Could not convert %1 to %2").arg(key, QString::fromUtf8(m_type.name())));
|
|
}
|
|
|
|
return { .value = decoded, .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
QStringList options;
|
|
options.reserve(m_metaEnum.keyCount());
|
|
for (int i = 0; i < m_metaEnum.keyCount(); ++i)
|
|
options << QString::fromUtf8(m_metaEnum.key(i));
|
|
|
|
return error(DiagnosticType::InvalidValue,
|
|
QStringLiteral("Invalid enum value %1. Expected one of %2").arg(key, options.join(u", "_s)));
|
|
}
|
|
|
|
template <typename Container>
|
|
ListCodec<Container>::ListCodec(const QMetaType& type, const ValueCodec* elementCodec)
|
|
: ValueCodec(type, ExpectedType::Array)
|
|
, m_elementCodec(elementCodec) {}
|
|
|
|
template <typename Container> QJsonValue ListCodec<Container>::encode(const QVariant& value) const {
|
|
QJsonArray array;
|
|
const auto list = value.value<Container>();
|
|
for (const auto& element : list)
|
|
array.append(m_elementCodec->encode(QVariant::fromValue(element)));
|
|
return array;
|
|
}
|
|
|
|
template <typename Container> DecodeResult ListCodec<Container>::decode(const QJsonValue& value) const {
|
|
if (!value.isArray())
|
|
return mismatch(ExpectedType::Array, value);
|
|
|
|
const auto array = value.toArray();
|
|
Container list;
|
|
list.reserve(array.size());
|
|
|
|
for (qsizetype i = 0; i < array.size(); ++i) {
|
|
auto result = m_elementCodec->decode(array.at(i));
|
|
|
|
// Reject the entire list if any element is invalid
|
|
if (result.error) {
|
|
result.indexPath.prepend(i);
|
|
return result;
|
|
}
|
|
|
|
list.append(result.value.value<Value>());
|
|
}
|
|
|
|
return { .value = QVariant::fromValue(list), .error = std::nullopt, .indexPath = {} };
|
|
}
|
|
|
|
UnionCodec::UnionCodec(const QList<const ValueCodec*>& alternatives)
|
|
: ValueCodec(QMetaType::fromType<QVariant>(), alternatives.first()->expected())
|
|
, m_alternatives(alternatives)
|
|
, m_expectedTypes(expectedTypesOf(alternatives)) {
|
|
m_byType.reserve(alternatives.size());
|
|
|
|
for (const auto* codec : alternatives)
|
|
m_byType.insert(codec->type().id(), codec);
|
|
}
|
|
|
|
QJsonValue UnionCodec::encode(const QVariant& value) const {
|
|
// An unset union is simply absent from the file
|
|
if (!value.isValid())
|
|
return QJsonValue::Undefined;
|
|
|
|
const auto* codec = m_byType.value(value.metaType().id());
|
|
if (!codec) {
|
|
qCWarning(lcSettings, "Cannot encode value of type %s, it is not one of the allowed types", value.typeName());
|
|
return QJsonValue::Undefined;
|
|
}
|
|
|
|
return codec->encode(value);
|
|
}
|
|
|
|
DecodeResult UnionCodec::decode(const QJsonValue& value) const {
|
|
std::optional<DecodeResult> best;
|
|
|
|
for (const auto* codec : m_alternatives) {
|
|
auto result = codec->decode(value);
|
|
|
|
// The first alternative to accept the value wins
|
|
if (!result.error)
|
|
return result;
|
|
|
|
// Non-nested type mismatch just means try another alternative
|
|
if (result.indexPath.isEmpty() && result.error->type == DiagnosticType::TypeMismatch)
|
|
continue;
|
|
|
|
// Report the deepest error that isn't a type mismatch
|
|
if (!best || result.indexPath.size() > best->indexPath.size())
|
|
best = std::move(result);
|
|
}
|
|
|
|
if (best)
|
|
return *best;
|
|
|
|
// Nothing matched the shape, so report every alternative
|
|
return mismatch(m_expectedTypes, value);
|
|
}
|
|
|
|
// Instantiated for types as needed
|
|
template class ListCodec<QStringList>;
|
|
template class ListCodec<QList<qreal>>;
|
|
|
|
} // namespace ZShell::settings
|