Files
z-bar-qt/Plugins/ZShell/Settings/codecs.cpp
T

421 lines
11 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