#include "codecs.hpp" #include #include #include #include #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& expected, const QJsonValue& value) { return { .value = QVariant(), .error = Diagnostic::mismatch(expected, value), .indexPath = {} }; } template ValueCodec* makeListCodec(const QMetaType& type) { const auto* elementCodec = ValueCodec::codecFor(QMetaType::fromType()); return elementCodec ? new ListCodec(type, elementCodec) : nullptr; } // Unions are keyed by their alternatives in order, since order decides which one wins QString unionKey(const QList& types) { QStringList ids; ids.reserve(types.size()); for (const auto& type : types) ids << QString::number(type.id()); return ids.join(u","_s); } QList expectedTypesOf(const QList& alternatives) { QList types; types.reserve(alternatives.size()); for (const auto* codec : alternatives) types << codec->expected(); return types; } using ListFactory = ValueCodec* (*)(const QMetaType&); const QHash& listFactories() { static const QHash k_factories{ { QMetaType::fromType().id(), &makeListCodec }, { QMetaType::fromType>().id(), &makeListCodec> }, }; 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 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& types) { // Cache for union codecs, keyed by their alternatives static QHash 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 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::min(); constexpr auto k_max = std::numeric_limits::max(); if (num < static_cast(k_min) || num > static_cast(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(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(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(), &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 ListCodec::ListCodec(const QMetaType& type, const ValueCodec* elementCodec) : ValueCodec(type, ExpectedType::Array) , m_elementCodec(elementCodec) {} template QJsonValue ListCodec::encode(const QVariant& value) const { QJsonArray array; const auto list = value.value(); for (const auto& element : list) array.append(m_elementCodec->encode(QVariant::fromValue(element))); return array; } template DecodeResult ListCodec::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()); } return { .value = QVariant::fromValue(list), .error = std::nullopt, .indexPath = {} }; } UnionCodec::UnionCodec(const QList& alternatives) : ValueCodec(QMetaType::fromType(), 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 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; template class ListCodec>; } // namespace ZShell::settings