From 0367e32eac019aa684b7245a5fa0efa655b976e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mika=C3=ABl=20Capelle?= Date: Fri, 9 Aug 2024 11:14:24 +0200 Subject: [PATCH] Implement requirements for extension. --- include/uibase/exceptions.h | 2 + include/uibase/extensions/extension.h | 72 ++++---- include/uibase/extensions/requirements.h | 28 ++- include/uibase/extensions/theme.h | 2 +- include/uibase/extensions/translation.h | 2 +- .../uibase/extensions/versionconstraints.h | 7 +- include/uibase/versioning.h | 3 +- src/CMakeLists.txt | 1 - src/extension.cpp | 138 +++++++------- src/requirements.cpp | 170 ++++++++++++++++-- src/uibase_en.ts | 1 - src/versionconstraints.cpp | 16 +- tests/CMakeLists.txt | 1 + tests/test_extensions.cpp | 54 ++++++ 14 files changed, 372 insertions(+), 125 deletions(-) create mode 100644 tests/test_extensions.cpp diff --git a/include/uibase/exceptions.h b/include/uibase/exceptions.h index c89a4833..b6425675 100644 --- a/include/uibase/exceptions.h +++ b/include/uibase/exceptions.h @@ -20,6 +20,8 @@ namespace MOBase class QDLLEXPORT Exception : public std::exception { public: + Exception(const char* text) : m_Message(text) {} + Exception(const std::string& text) : m_Message(QByteArray::fromStdString(text)) {} Exception(const QString& text) : m_Message(text.toUtf8()) {} virtual const char* what() const noexcept override { return m_Message.constData(); } diff --git a/include/uibase/extensions/extension.h b/include/uibase/extensions/extension.h index 83cb6633..c26f2bad 100644 --- a/include/uibase/extensions/extension.h +++ b/include/uibase/extensions/extension.h @@ -8,20 +8,25 @@ #include #include -#include "dllimport.h" -#include "iplugingame.h" +#include "../dllimport.h" +#include "../iplugingame.h" +#include "../versioning.h" #include "requirements.h" #include "theme.h" #include "translation.h" -#include "versioninfo.h" namespace MOBase { class IExtension; +class InvalidExtensionMetaDataException : public Exception +{ +public: + using Exception::Exception; +}; + enum class ExtensionType { - INVALID, THEME, TRANSLATION, PLUGIN, @@ -48,10 +53,6 @@ class QDLLEXPORT ExtensionContributor class QDLLEXPORT ExtensionMetaData { public: - // check if that metadata object is valid - // - bool isValid() const; - // retrieve the identifier of the extension // const auto& identifier() const { return m_Identifier; } @@ -72,7 +73,7 @@ class QDLLEXPORT ExtensionMetaData // auto type() const { return m_Type; } - // retrieve the description of the extension. + // retrieve the description of the extension // auto description() const { return localized(m_Description); } @@ -80,10 +81,14 @@ class QDLLEXPORT ExtensionMetaData // const auto& icon() const { return m_Icon; } - // retrieve the version of the extension. + // retrieve the version of the extension // const auto& version() const { return m_Version; } + // retrieve the requirements of the extension + // + const auto& requirements() const { return m_Requirements; } + // retrieve the raw JSON metadata, this is mostly useful for specific extension type // to extract custom parts // @@ -92,8 +97,8 @@ class QDLLEXPORT ExtensionMetaData // retrieve the content objects of the extension QJsonObject content() const; -private: - QString localized(QString const& value) const; +protected: + ExtensionMetaData(std::filesystem::path const& path, const QJsonObject& jsonData); private: friend class ExtensionFactory; @@ -101,9 +106,7 @@ class QDLLEXPORT ExtensionMetaData constexpr static const char* DEFAULT_TRANSLATIONS_FOLDER = "translations"; constexpr static const char* DEFAULT_STYLESHEET_PATH = "stylesheets"; - ExtensionType parseType(QString const& value) const; - - ExtensionMetaData(std::filesystem::path const& path, const QJsonObject& jsonData); + std::optional parseType(QString const& value) const; private: QJsonObject m_JsonData; @@ -116,10 +119,13 @@ class QDLLEXPORT ExtensionMetaData ExtensionType m_Type; QString m_Description; QIcon m_Icon; - VersionInfo m_Version; + Version m_Version; + std::vector m_Requirements; std::filesystem::path m_TranslationFilesPrefix; std::filesystem::path m_StyleSheetFilePath; + + QString localized(QString const& value) const; }; class QDLLEXPORT IExtension @@ -133,16 +139,12 @@ class QDLLEXPORT IExtension // const auto& metadata() const { return m_MetaData; } - // retrieve the requirements of the extension - // - const auto& requirements() const { return m_Requirements; } - public: virtual ~IExtension() {} IExtension& operator=(const IExtension&) = delete; protected: - IExtension(std::filesystem::path path, ExtensionMetaData metadata); + IExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); public: IExtension(const IExtension&) = default; @@ -150,7 +152,6 @@ class QDLLEXPORT IExtension private: std::filesystem::path m_Path; ExtensionMetaData m_MetaData; - std::vector m_Requirements; }; // factory for extensions @@ -161,13 +162,14 @@ class QDLLEXPORT ExtensionFactory // load an extension from the given directory, return a null-pointer if the extension // could not be load // - static std::unique_ptr loadExtension(std::filesystem::path directory); + static std::unique_ptr + loadExtension(std::filesystem::path const& directory); private: // load an extension from the given directory // - static std::unique_ptr loadExtension(std::filesystem::path directory, - ExtensionMetaData metadata); + static std::unique_ptr + loadExtension(std::filesystem::path const& directory, ExtensionMetaData&& metadata); }; // theme extension that provides one or more base themes for MO2 @@ -180,12 +182,12 @@ class QDLLEXPORT ThemeExtension : public IExtension const auto& themes() const { return m_Themes; } private: - ThemeExtension(std::filesystem::path path, ExtensionMetaData metadata, + ThemeExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata, std::vector> themes); friend class ExtensionFactory; - static std::unique_ptr loadExtension(std::filesystem::path path, - ExtensionMetaData metadata); + static std::unique_ptr + loadExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); static std::shared_ptr parseTheme(std::filesystem::path const& extensionFolder, const QString& identifier, @@ -205,12 +207,12 @@ class QDLLEXPORT TranslationExtension : public IExtension const auto& translations() const { return m_Translations; } private: - TranslationExtension(std::filesystem::path path, ExtensionMetaData metadata, + TranslationExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata, std::vector> translations); friend class ExtensionFactory; static std::unique_ptr - loadExtension(std::filesystem::path path, ExtensionMetaData metadata); + loadExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); static std::shared_ptr parseTranslation(std::filesystem::path const& extensionFolder, @@ -241,14 +243,14 @@ class QDLLEXPORT PluginExtension : public IExtension protected: PluginExtension( - std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::filesystem::path const& path, ExtensionMetaData&& metadata, bool autodetect, std::map plugins, std::vector> themeAdditions, std::vector> translationAdditions); friend class ExtensionFactory; - static std::unique_ptr loadExtension(std::filesystem::path path, - ExtensionMetaData metadata); + static std::unique_ptr + loadExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata); private: // auto-detect plugins @@ -271,8 +273,8 @@ class QDLLEXPORT GameExtension : public PluginExtension GameExtension(PluginExtension&& pluginExtension); friend class ExtensionFactory; - static std::unique_ptr loadExtension(std::filesystem::path path, - ExtensionMetaData metadata); + static std::unique_ptr loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata); }; } // namespace MOBase diff --git a/include/uibase/extensions/requirements.h b/include/uibase/extensions/requirements.h index 1d71d52d..2a10d5de 100644 --- a/include/uibase/extensions/requirements.h +++ b/include/uibase/extensions/requirements.h @@ -6,13 +6,26 @@ #include #include -#include "dllimport.h" +#include "../dllimport.h" +#include "../exceptions.h" namespace MOBase { class IOrganizer; class ExtensionMetaData; +class InvalidRequirementException : public Exception +{ +public: + using Exception::Exception; +}; + +class InvalidRequirementsException : public Exception +{ +public: + using Exception::Exception; +}; + class ExtensionRequirementImpl; // extension requirements @@ -44,13 +57,22 @@ class QDLLEXPORT ExtensionRequirement // bool check(IOrganizer* organizer) const; + // retrieve the type of this extension + // + Type type() const; + + // retrieve a textual representation of this requirement, e.g. "ModOrganizer 2.5.4" + // for a requirement that requires MO2 2.5.4 + // + QString string() const; + +public: ~ExtensionRequirement(); private: friend class ExtensionRequirementFactory; ExtensionRequirement(std::shared_ptr impl); - std::shared_ptr m_Impl; }; @@ -62,7 +84,7 @@ class QDLLEXPORT ExtensionRequirementFactory // extract requirements from the given metadata // static std::vector - parseRequirements(const ExtensionMetaData& metadata); + parseRequirements(const QJsonValue& json_requirements); private: }; diff --git a/include/uibase/extensions/theme.h b/include/uibase/extensions/theme.h index 624f1a01..1802ca14 100644 --- a/include/uibase/extensions/theme.h +++ b/include/uibase/extensions/theme.h @@ -6,7 +6,7 @@ #include -#include "dllimport.h" +#include "../dllimport.h" namespace MOBase { diff --git a/include/uibase/extensions/translation.h b/include/uibase/extensions/translation.h index b1dd66f9..dafb3371 100644 --- a/include/uibase/extensions/translation.h +++ b/include/uibase/extensions/translation.h @@ -4,7 +4,7 @@ #include #include -#include "dllimport.h" +#include "../dllimport.h" namespace MOBase { diff --git a/include/uibase/extensions/versionconstraints.h b/include/uibase/extensions/versionconstraints.h index a66cbb59..61791fcb 100644 --- a/include/uibase/extensions/versionconstraints.h +++ b/include/uibase/extensions/versionconstraints.h @@ -57,13 +57,18 @@ class QDLLEXPORT VersionConstraints public: // construct a set of constraints // - VersionConstraints(std::vector constraints); + VersionConstraints(QString const& repr, std::vector constraints); // check if the given version matches the set of constraints // bool matches(Version const& version) const; + // retrieve a string representation of this set of constraints + // + auto string() const { return m_Repr; } + private: + QString m_Repr; std::vector m_Constraints; }; diff --git a/include/uibase/versioning.h b/include/uibase/versioning.h index c4f95419..ab3d7b14 100644 --- a/include/uibase/versioning.h +++ b/include/uibase/versioning.h @@ -76,7 +76,8 @@ class QDLLEXPORT Version // do not add metadata even if present // - NoMetadata = 0b1000 + NoMetadata = 0b1000, + }; Q_DECLARE_FLAGS(FormatModes, FormatMode); diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 8e3379ec..ebd77919 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -117,7 +117,6 @@ mo2_target_sources(uibase errorcodes.cpp eventfilter.cpp executableinfo.cpp - extension.cpp filesystemutilities.cpp guessedvalue.cpp json.cpp diff --git a/src/extension.cpp b/src/extension.cpp index 0b2f83eb..761db63a 100644 --- a/src/extension.cpp +++ b/src/extension.cpp @@ -65,15 +65,39 @@ ExtensionContributor::ExtensionContributor(QString name) : m_Name{name} {} ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, QJsonObject const& jsonData) - : m_JsonData{jsonData} + : m_JsonData{jsonData}, m_Version{0, 0, 0}, m_Requirements{} { // read basic fields - m_Identifier = jsonData["id"].toString(); - m_Type = parseType(jsonData["type"].toString()); - m_Name = jsonData["name"].toString(); + m_Identifier = jsonData["id"].toString(); + if (m_Identifier.isEmpty()) { + throw InvalidExtensionMetaDataException("missing identifier"); + } + + { + const auto maybeType = parseType(jsonData["type"].toString()); + if (!maybeType.has_value()) { + throw InvalidExtensionMetaDataException( + std::format("invalid or missing type '{}'", jsonData["type"].toString())); + } + + m_Type = *maybeType; + } + + m_Name = jsonData["name"].toString(); + if (m_Name.isEmpty()) { + throw InvalidExtensionMetaDataException("missing name"); + } + m_Author = parseContributor(jsonData["author"]); m_Description = jsonData["description"].toString(); - m_Version.parse(jsonData["version"].toString("0.0.0")); + + try { + m_Version = Version::parse(jsonData["version"].toString("0.0.0"), + Version::ParseMode::SemVer); + } catch (InvalidVersionException const& ex) { + throw InvalidExtensionMetaDataException( + std::format("invalid or missing version '{}'", jsonData["version"].toString())); + } // TODO: name of the key // translation context @@ -86,38 +110,25 @@ ExtensionMetaData::ExtensionMetaData(std::filesystem::path const& path, } } - // TODO: move code in a better place or use a custom icon - if (m_Icon.isNull()) { - const QImage baseIcon(":/MO/gui/app_icon"); - QImage grayIcon = baseIcon.convertToFormat(QImage::Format_ARGB32); - { - for (int y = 0; y < grayIcon.height(); ++y) { - QRgb* scanLine = (QRgb*)grayIcon.scanLine(y); - for (int x = 0; x < grayIcon.width(); ++x) { - QRgb pixel = *scanLine; - uint ci = uint(qGray(pixel)); - *scanLine = qRgba(ci, ci, ci, qAlpha(pixel) / 3); - ++scanLine; - } - } - } - m_Icon = QIcon(QPixmap::fromImage(grayIcon)); - } - if (jsonData.contains("contributors")) { for (const auto& jsonContributor : jsonData["contributors"].toArray()) { m_Contributors.push_back(parseContributor(jsonContributor)); } } -} -bool ExtensionMetaData::isValid() const -{ - return !m_Identifier.isEmpty() && !m_Name.isEmpty() && m_Version.isValid() && - m_Type != ExtensionType::INVALID; + if (jsonData.contains("requirements")) { + try { + m_Requirements = + ExtensionRequirementFactory::parseRequirements(jsonData["requirements"]); + } catch (InvalidRequirementException const& ex) { + throw InvalidExtensionMetaDataException(ex.what()); + } catch (InvalidRequirementsException const& ex) { + throw InvalidExtensionMetaDataException(ex.what()); + } + } } -ExtensionType ExtensionMetaData::parseType(QString const& value) const +std::optional ExtensionMetaData::parseType(QString const& value) const { std::map stringToTypes{ {"theme", ExtensionType::THEME}, @@ -125,7 +136,7 @@ ExtensionType ExtensionMetaData::parseType(QString const& value) const {"plugin", ExtensionType::PLUGIN}, {"game", ExtensionType::GAME}}; - auto type = ExtensionType::INVALID; + std::optional type; for (auto& [k, v] : stringToTypes) { if (k.compare(value, Qt::CaseInsensitive) == 0) { type = v; @@ -163,13 +174,12 @@ QJsonObject ExtensionMetaData::content() const return value.toObject(); } -IExtension::IExtension(std::filesystem::path path, ExtensionMetaData metadata) - : m_Path{std::move(path)}, m_MetaData{std::move(metadata)}, - m_Requirements{ExtensionRequirementFactory::parseRequirements(m_MetaData)} +IExtension::IExtension(std::filesystem::path const& path, ExtensionMetaData&& metadata) + : m_Path{path}, m_MetaData{std::move(metadata)} {} std::unique_ptr -ExtensionFactory::loadExtension(std::filesystem::path directory) +ExtensionFactory::loadExtension(std::filesystem::path const& directory) { const auto metadataPath = directory / METADATA_FILENAME; @@ -197,44 +207,44 @@ ExtensionFactory::loadExtension(std::filesystem::path directory) return nullptr; } - return loadExtension(std::move(directory), - ExtensionMetaData(directory, jsonMetaData.object())); + try { + return loadExtension(directory, + ExtensionMetaData(directory, jsonMetaData.object())); + } catch (InvalidExtensionMetaDataException const& ex) { + log::warn("failed to load extension from '{}': invalid metadata ({})", + directory.native(), ex.what()); + return nullptr; + } } std::unique_ptr -ExtensionFactory::loadExtension(std::filesystem::path directory, - ExtensionMetaData metadata) +ExtensionFactory::loadExtension(std::filesystem::path const& directory, + ExtensionMetaData&& metadata) { - if (!metadata.isValid()) { - log::warn("failed to load extension from '{}': invalid metadata", - directory.native()); - return nullptr; - } - switch (metadata.type()) { case ExtensionType::THEME: - return ThemeExtension::loadExtension(std::move(directory), std::move(metadata)); + return ThemeExtension::loadExtension(directory, std::move(metadata)); case ExtensionType::TRANSLATION: - return TranslationExtension::loadExtension(std::move(directory), - std::move(metadata)); + return TranslationExtension::loadExtension(directory, std::move(metadata)); case ExtensionType::PLUGIN: - return PluginExtension::loadExtension(std::move(directory), std::move(metadata)); + return PluginExtension::loadExtension(directory, std::move(metadata)); case ExtensionType::GAME: - return GameExtension::loadExtension(std::move(directory), std::move(metadata)); - case ExtensionType::INVALID: + return GameExtension::loadExtension(directory, std::move(metadata)); default: log::warn("failed to load extension from '{}': invalid type", directory.native()); return nullptr; } } -ThemeExtension::ThemeExtension(std::filesystem::path path, ExtensionMetaData metadata, +ThemeExtension::ThemeExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata, std::vector> themes) - : IExtension{std::move(path), std::move(metadata)}, m_Themes{std::move(themes)} + : IExtension{path, std::move(metadata)}, m_Themes{std::move(themes)} {} std::unique_ptr -ThemeExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) +ThemeExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { std::vector> themes; const auto& jsonThemes = metadata.content()["themes"].toObject(); @@ -253,7 +263,7 @@ ThemeExtension::loadExtension(std::filesystem::path path, ExtensionMetaData meta } return std::unique_ptr{ - new ThemeExtension(path, metadata, std::move(themes))}; + new ThemeExtension(path, std::move(metadata), std::move(themes))}; } std::shared_ptr @@ -273,15 +283,15 @@ ThemeExtension::parseTheme(std::filesystem::path const& extensionFolder, } TranslationExtension::TranslationExtension( - std::filesystem::path path, ExtensionMetaData metadata, + std::filesystem::path const& path, ExtensionMetaData&& metadata, std::vector> translations) : IExtension{std::move(path), std::move(metadata)}, m_Translations(std::move(translations)) {} std::unique_ptr -TranslationExtension::loadExtension(std::filesystem::path path, - ExtensionMetaData metadata) +TranslationExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { std::vector> translations; const auto& jsonTranslations = metadata.content()["translations"].toObject(); @@ -300,7 +310,7 @@ TranslationExtension::loadExtension(std::filesystem::path path, } return std::unique_ptr{ - new TranslationExtension(path, metadata, std::move(translations))}; + new TranslationExtension(path, std::move(metadata), std::move(translations))}; } std::shared_ptr @@ -333,17 +343,18 @@ TranslationExtension::parseTranslation(std::filesystem::path const& extensionFol } PluginExtension::PluginExtension( - std::filesystem::path path, ExtensionMetaData metadata, bool autodetect, + std::filesystem::path const& path, ExtensionMetaData&& metadata, bool autodetect, std::map plugins, std::vector> themeAdditions, std::vector> translationAdditions) - : IExtension(std::move(path), std::move(metadata)), m_AutoDetect{autodetect}, + : IExtension(path, std::move(metadata)), m_AutoDetect{autodetect}, m_Plugins{std::move(plugins)}, m_ThemeAdditions{std::move(themeAdditions)}, m_TranslationAdditions{std::move(translationAdditions)} {} std::unique_ptr -PluginExtension::loadExtension(std::filesystem::path path, ExtensionMetaData metadata) +PluginExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { namespace fs = std::filesystem; @@ -450,8 +461,9 @@ GameExtension::GameExtension(PluginExtension&& pluginExtension) : PluginExtension(std::move(pluginExtension)) {} -std::unique_ptr GameExtension::loadExtension(std::filesystem::path path, - ExtensionMetaData metadata) +std::unique_ptr +GameExtension::loadExtension(std::filesystem::path const& path, + ExtensionMetaData&& metadata) { auto extension = PluginExtension::loadExtension(std::move(path), std::move(metadata)); return extension diff --git a/src/requirements.cpp b/src/requirements.cpp index 894f56e7..11bd81c5 100644 --- a/src/requirements.cpp +++ b/src/requirements.cpp @@ -3,6 +3,8 @@ #include #include "extensions/extension.h" +#include "extensions/iextensionlist.h" +#include "extensions/versionconstraints.h" #include "imoinfo.h" #include "log.h" @@ -10,7 +12,91 @@ namespace MOBase { class ExtensionRequirementImpl -{}; +{ +public: + using Type = ExtensionRequirement::Type; + +public: + virtual bool check(IOrganizer* organizer) const = 0; + virtual Type type() const = 0; + virtual QString string() const = 0; + virtual ~ExtensionRequirementImpl() = default; +}; + +// requirement for the version of MO2 itself +// +class CoreVersionExtensionRequirement : public ExtensionRequirementImpl +{ +public: + CoreVersionExtensionRequirement(VersionConstraints const& constraints) + : m_Constraints{constraints} + {} + + bool check(IOrganizer* organizer) const override + { + return m_Constraints.matches(organizer->version()); + } + + Type type() const override { return Type::VERSION; } + + QString string() const override + { + return QString("ModOrganizer2 %1").arg(m_Constraints.string()); + } + +private: + VersionConstraints m_Constraints; +}; + +// requirement for another extension +// +class DependencyExtensionRequirement : public ExtensionRequirementImpl +{ +public: + DependencyExtensionRequirement(QString const& extension, + VersionConstraints const& constraints) + : m_Extension{extension}, m_Constraints{constraints} + {} + + bool check(IOrganizer* organizer) const override + { + return organizer->extensionList().enabled(m_Extension) && + m_Constraints.matches( + organizer->extensionList().get(m_Extension).metadata().version()); + } + + Type type() const override { return Type::DEPENDENCY; } + + QString string() const override + { + return QString("%1 %2").arg(m_Extension, m_Constraints.string()); + } + +private: + QString m_Extension; + VersionConstraints m_Constraints; +}; + +// requirement for games +// +class GameExtensionRequirement : public ExtensionRequirementImpl +{ +public: + GameExtensionRequirement(QStringList const& games) : m_Games{games} {} + + bool check(IOrganizer* organizer) const override + { + return organizer->managedGame() && + m_Games.contains(organizer->managedGame()->gameName()); + } + + Type type() const override { return Type::GAME; } + + QString string() const override { return m_Games.join(", "); } + +private: + QStringList m_Games; +}; } // namespace MOBase @@ -23,29 +109,89 @@ ExtensionRequirement::ExtensionRequirement( ExtensionRequirement::~ExtensionRequirement() = default; -bool ExtensionRequirement::check([[maybe_unused]] IOrganizer* organizer) const +bool ExtensionRequirement::check(IOrganizer* organizer) const { - return true; + return m_Impl->check(organizer); } -std::vector -ExtensionRequirementFactory::parseRequirements(const ExtensionMetaData& metadata) +ExtensionRequirement::Type ExtensionRequirement::type() const +{ + return m_Impl->type(); +} + +QString ExtensionRequirement::string() const +{ + return m_Impl->string(); +} + +namespace +{ +std::optional parseType(QString const& value) { - if (!metadata.json().contains("requirements")) { - return {}; + std::map stringToTypes{ + {"game", ExtensionRequirement::Type::GAME}, + {"extension", ExtensionRequirement::Type::DEPENDENCY}, + {"version", ExtensionRequirement::Type::VERSION}}; + + std::optional type; + for (auto& [k, v] : stringToTypes) { + if (k.compare(value, Qt::CaseInsensitive) == 0) { + type = v; + break; + } } - const auto json_requirements = metadata.json()["requirements"]; + return type; +} +} // namespace +std::vector +ExtensionRequirementFactory::parseRequirements(const QJsonValue& json_requirements) +{ if (!json_requirements.isArray()) { - log::warn("expected array of requirements for extension '{}', found '{}'", - metadata.identifier(), json_requirements.type()); - return {}; + throw InvalidRequirementsException("expected an array of requirements"); } std::vector requirements; for (const auto& json_requirement : json_requirements.toArray()) { - // TODO + if (!json_requirement.isObject()) { + throw InvalidRequirementException("invalid requirement"); + } + + auto json_object = json_requirement.toObject(); + + const auto type = parseType(json_object["type"].toString()); + if (!type.has_value()) { + throw InvalidRequirementException("missing requirement type"); + } + + try { + switch (*type) { + case ExtensionRequirement::Type::GAME: + if (!json_object.contains("games") || !json_object["games"].isArray()) { + throw InvalidRequirementException("invalid requirement"); + } + requirements.push_back( + ExtensionRequirement(std::make_shared( + json_object["games"].toVariant().toStringList()))); + break; + case ExtensionRequirement::Type::DEPENDENCY: + requirements.push_back( + ExtensionRequirement(std::make_shared( + json_object["extension"].toString(), + VersionConstraints::parse(json_object["version"].toString(), + Version::ParseMode::SemVer)))); + break; + case ExtensionRequirement::Type::VERSION: + requirements.push_back(ExtensionRequirement( + std::make_shared(VersionConstraints::parse( + json_object["version"].toString(), Version::ParseMode::MO2)))); + break; + } + } catch (InvalidConstraintException const& ex) { + throw InvalidRequirementException( + std::format("invalid requirement constraints: {}", ex.what())); + } } return requirements; diff --git a/src/uibase_en.ts b/src/uibase_en.ts index d5dd17a5..1156e86c 100644 --- a/src/uibase_en.ts +++ b/src/uibase_en.ts @@ -181,7 +181,6 @@ Failed to save '%1', could not create a temporary file: %2 (error %3) - Failed to save '{}', could not create a temporary file: {} (error {}) diff --git a/src/versionconstraints.cpp b/src/versionconstraints.cpp index 69f09be4..1cfc9dd7 100644 --- a/src/versionconstraints.cpp +++ b/src/versionconstraints.cpp @@ -169,7 +169,6 @@ VersionConstraint VersionConstraint::parse(QString const& value, } } - constexpr auto min_int = std::numeric_limits::min(); constexpr auto max_int = std::numeric_limits::max(); std::shared_ptr impl; @@ -284,10 +283,14 @@ VersionConstraints VersionConstraints::parse(QString const& value, Version::ParseMode mode) { std::vector constraints; - for (const auto& part : value.split(",")) { - constraints.push_back(VersionConstraint::parse(part.trimmed(), mode)); + auto parts = value.split(","); + for (auto& part : parts) { + // replace the part in-place to create a proper representation + part = part.simplified().replace(" ", ""); + + constraints.push_back(VersionConstraint::parse(part, mode)); } - return VersionConstraints(std::move(constraints)); + return VersionConstraints(parts.join(", "), std::move(constraints)); } bool VersionConstraints::matches(Version const& version) const @@ -298,8 +301,9 @@ bool VersionConstraints::matches(Version const& version) const }); } -VersionConstraints::VersionConstraints(std::vector checkers) - : m_Constraints{std::move(checkers)} +VersionConstraints::VersionConstraints(QString const& repr, + std::vector checkers) + : m_Repr{repr}, m_Constraints{std::move(checkers)} {} } // namespace MOBase diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7ff9e192..b9845270 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -8,6 +8,7 @@ target_sources(uibase-tests test_ifiletree.cpp test_strings.cpp test_versioning.cpp + test_extensions.cpp ) mo2_configure_tests(uibase-tests NO_SOURCES NO_MAIN NO_MOCK WARNINGS 4) target_link_libraries(uibase-tests PRIVATE uibase) diff --git a/tests/test_extensions.cpp b/tests/test_extensions.cpp new file mode 100644 index 00000000..4e767282 --- /dev/null +++ b/tests/test_extensions.cpp @@ -0,0 +1,54 @@ +#pragma warning(push) +#pragma warning(disable : 4668) +#include +#pragma warning(pop) + +#include + +#include + +#include + +using namespace MOBase; + +class TestMetaData : public ExtensionMetaData +{ +public: + TestMetaData(std::filesystem::path const& path, QByteArray const& metadata) + : ExtensionMetaData(path, QJsonDocument::fromJson(metadata).object()) + {} +}; + +TEST(ExtensionsTest, MetaData) +{ + const auto metadata = TestMetaData({}, R"({ + "id": "mo2-game-bethesda", + "name": "Elder Scrolls & Fallout Games", + "version": "1.0.0", + "description": "ModOrganizer2 support for The Elder Scrolls & Fallout games.", + "author": { + "name": "Mod Organizer 2", + "homepage": "https://www.modorganizer.org/" + }, + "icon": "./tests/icon.png", + "contributors": [ + "AL", + "AnyOldName3", + "Holt59", + "Silarn" + ], + "type": "game", + "content": { + "plugins": { + "autodetect": true + }, + "translations": { + "autodetect": "translations" + } + } +})"); + + EXPECT_EQ("mo2-game-bethesda", metadata.identifier()); + EXPECT_EQ("Elder Scrolls & Fallout Games", metadata.name()); + EXPECT_FALSE(metadata.icon().isNull()); +}