Files
z-bar-qt/Plugins/ZShell/Settings/codecs.cpp
T
zach 9ea1f846ae
C++ / fmt (pull_request) Failing after 5s
C++ / build (pull_request) Failing after 10s
C++ / clang-tidy (pull_request) Failing after 11s
JS/TS / fmt (pull_request) Successful in 9s
JS/TS / lint (pull_request) Successful in 10s
Python / static (pull_request) Successful in 34s
Rust / build (pull_request) Successful in 59s
Python / verify (pull_request) Successful in 1m48s
Rust / fmt (pull_request) Successful in 47s
Rust / clippy (pull_request) Successful in 1m6s
more unfinished changes, process tab for dashboard
2026-09-25 16:51:28 +02:00

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