diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp index a4a31d78d..198fa259b 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp +++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.cpp @@ -1,17 +1,9 @@ #include "dlg_convert_deck_to_cod_format.h" -#include "../../../client/settings/cache_settings.h" -#include "../../deck_loader/deck_loader.h" - #include #include -#include -#include -#include #include -#include #include -#include DialogConvertDeckToCodFormat::DialogConvertDeckToCodFormat(QWidget *parent) : QDialog(parent) { @@ -46,71 +38,3 @@ bool DialogConvertDeckToCodFormat::dontAskAgain() const { return dontAskAgainCheckbox->isChecked(); } - -namespace -{ - -bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) -{ - QFileInfo fileInfo(filePath); - QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); - - if (QFile::exists(newFileName)) { - QMessageBox::StandardButton reply = - QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), - QObject::tr("A .cod version of this deck already exists. Overwrite it?"), - QMessageBox::Yes | QMessageBox::No); - return reply == QMessageBox::Yes; - } - return true; // Safe to proceed -} - -} // namespace - -bool DialogConvertDeckToCodFormat::promptIfRequired(QWidget *parent, - const QString &filePath, - const std::function &convert) -{ - if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) { - return true; - } - - // Retrieve saved preference if the prompt is disabled - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { - if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { - return false; - } - - if (!confirmOverwriteIfExists(parent, filePath)) { - return false; - } - - return convert(); - } - - // Show the dialog to the user - DialogConvertDeckToCodFormat conversionDialog(parent); - if (conversionDialog.exec() != QDialog::Accepted) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( - !conversionDialog.dontAskAgain()); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); - - return false; - } - - // Try to convert file - if (!confirmOverwriteIfExists(parent, filePath)) { - return false; - } - - if (!convert()) { - return false; - } - - if (conversionDialog.dontAskAgain()) { - SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); - SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); - } - - return true; -} diff --git a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h index 526582135..6642ad8c6 100644 --- a/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h +++ b/cockatrice/src/interface/widgets/dialogs/dlg_convert_deck_to_cod_format.h @@ -13,9 +13,6 @@ #include #include #include -#include - -class QWidget; class DialogConvertDeckToCodFormat : public QDialog { @@ -27,21 +24,6 @@ public: [[nodiscard]] bool dontAskAgain() const; - /** - * @brief Checks whether the deck file at \a filePath can store tags. - * - * If the file is not a .cod deck, prompts the user for conversion to the - * Cockatrice format, honoring the saved "always convert / don't ask again" - * preference. On acceptance \a convert is called to perform the conversion. - * - * @param parent The widget to parent the prompt to. - * @param filePath The path of the deck file to check. - * @param convert Called to convert the deck once the user agrees. - * @return true if tags can be stored (no conversion needed, or the conversion - * was performed), false if the user declined to convert. - */ - static bool promptIfRequired(QWidget *parent, const QString &filePath, const std::function &convert); - private: QVBoxLayout *layout; QLabel *label; diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp index fb610e814..f6f34a6a5 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.cpp @@ -525,13 +525,6 @@ void UserInfoPopup::rebuildActionButtons(const ServerInfo_User &userInfo, bool o connect(games, &QPushButton::clicked, this, [this, name] { emit showGamesRequested(name); }); add(games); - // ── Invite (only while the inviter has a joinable game for this user) ──── - if (!isSelf && online && gameInviteAvailable && gameInviteAvailable(name)) { - auto *invite = makeBtn(tr("Invite"), tr("Invite to your game"), actionArea, theme); - connect(invite, &QPushButton::clicked, this, [this, name] { emit inviteRequested(name); }); - add(invite); - } - // ── Buddy / ignore (registered users only) ──────────────────────────────── if (!isSelf && isReg) { if (isBuddy) { diff --git a/cockatrice/src/interface/widgets/server/user/user_info_popup.h b/cockatrice/src/interface/widgets/server/user/user_info_popup.h index ed7320fba..02cc2b44e 100644 --- a/cockatrice/src/interface/widgets/server/user/user_info_popup.h +++ b/cockatrice/src/interface/widgets/server/user/user_info_popup.h @@ -9,7 +9,6 @@ #include #include #include -#include #include #include #include @@ -150,17 +149,6 @@ public: /** Re-pulls the avatar/card art for the currently shown user (e.g. after it loads). */ void refreshHeader(); - /** - * Sets a predicate evaluated on every action-button rebuild. It receives - * the name of the user the popup currently shows; when it returns true an - * "Invite" button is shown. The popup itself never resolves the invite - * link, it just forwards the request. - */ - void setGameInviteAvailable(std::function available) - { - gameInviteAvailable = std::move(available); - } - signals: void mouseEnteredPopup(); void mouseLeftPopup(); @@ -171,7 +159,6 @@ signals: // ── Action signals — connect to UserContextMenu::exec*() ────────────────── void chatRequested(const QString &userName); - void inviteRequested(const QString &userName); void detailsRequested(const QString &userName); void showGamesRequested(const QString &userName); void addBuddyRequested(const QString &userName); @@ -213,7 +200,6 @@ private: QString currentUser; ServerInfo_User currentUserInfo; bool currentOnline = false; - std::function gameInviteAvailable; UserInfoHeaderWidget *header; QWidget *actionArea; ///< rebuilt per user diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp index a8c99c979..2cacfc4f9 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.cpp @@ -345,11 +345,6 @@ UserListWidget::UserListWidget(TabSupervisor *_tabSupervisor, &cardArtProvider->cache(), &cardArtParamsMap, window()); // parented to main window so it floats above siblings - // The invite availability is scoped to the room this list belongs to, - // and gated on the room's buddy-only setting for the hovered user. - userInfoPopup->setGameInviteAvailable( - [this](const QString &userName) { return userContextMenu->hasGameInviteLink(userName); }); - userInfoPopup->hide(); userInfoPopup->setWindowOpacity(0.0); userInfoPopup->installEventFilter(this); @@ -667,8 +662,6 @@ void UserListWidget::connectPopupSignals() // Wire all action signals to UserContextMenu::exec*() connect(userInfoPopup, &UserInfoPopup::chatRequested, userContextMenu, &UserContextMenu::execChat); - connect(userInfoPopup, &UserInfoPopup::inviteRequested, this, - [this](const QString &userName) { userContextMenu->execInvite(userName); }); connect(userInfoPopup, &UserInfoPopup::detailsRequested, userContextMenu, &UserContextMenu::execDetails); connect(userInfoPopup, &UserInfoPopup::showGamesRequested, userContextMenu, &UserContextMenu::execShowGames); connect(userInfoPopup, &UserInfoPopup::addBuddyRequested, userContextMenu, &UserContextMenu::execAddToBuddy); diff --git a/cockatrice/src/interface/widgets/server/user/user_list_widget.h b/cockatrice/src/interface/widgets/server/user/user_list_widget.h index 412271160..7531ef925 100644 --- a/cockatrice/src/interface/widgets/server/user/user_list_widget.h +++ b/cockatrice/src/interface/widgets/server/user/user_list_widget.h @@ -22,7 +22,6 @@ #include #include #include -#include #include class QTreeWidget; diff --git a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp index b0dac3e7c..f96c139b3 100644 --- a/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp +++ b/cockatrice/src/interface/widgets/tabs/tab_supervisor.cpp @@ -1091,8 +1091,7 @@ QList TabSupervisor::getGameInviteLinksForRoom(int roomId) con // The inviter may be in several games of the same room (hosting one and // spectating another, for example). Return every game so the caller can // let the user choose which one to invite to. - for (auto it = gameTabs.cbegin(); it != gameTabs.cend(); ++it) { - TabGame *tab = it.value(); + for (TabGame *tab : gameTabs) { GameMetaInfo *metaInfo = tab->getGame()->getGameMetaInfo(); if (metaInfo->proto().room_id() != roomId) { continue; diff --git a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp index 876fbf6ad..04dcdf7f2 100644 --- a/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp +++ b/cockatrice/src/interface/widgets/visual_deck_storage/deck_preview/deck_preview_widget.cpp @@ -10,6 +10,8 @@ #include "../visual_deck_storage_widget.h" #include "deck_preview_deck_tags_display_widget.h" +#include +#include #include #include #include @@ -497,6 +499,21 @@ void DeckPreviewWidget::actDeleteFile() // The folder widget removes this preview once the row is gone. } +static bool confirmOverwriteIfExists(QWidget *parent, const QString &filePath) +{ + QFileInfo fileInfo(filePath); + QString newFileName = QDir::toNativeSeparators(fileInfo.path() + "/" + fileInfo.completeBaseName() + ".cod"); + + if (QFile::exists(newFileName)) { + QMessageBox::StandardButton reply = + QMessageBox::question(parent, QObject::tr("Overwrite Existing File?"), + QObject::tr("A .cod version of this deck already exists. Overwrite it?"), + QMessageBox::Yes | QMessageBox::No); + return reply == QMessageBox::Yes; + } + return true; // Safe to proceed +} + /** * Checks if the deck's file format supports tags. * If not, then prompt the user for file conversion. @@ -504,8 +521,45 @@ void DeckPreviewWidget::actDeleteFile() */ bool DeckPreviewWidget::promptFileConversionIfRequired() { - return DialogConvertDeckToCodFormat::promptIfRequired(this, filePath, [this] { + if (DeckFileFormat::getFormatFromName(filePath) == DeckFileFormat::Cockatrice) { + return true; + } + + // Retrieve saved preference if the prompt is disabled + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStoragePromptForConversion()) { + if (!SettingsCache::instance().visualDeckStorage().getVisualDeckStorageAlwaysConvert()) { + return false; + } + + if (!confirmOverwriteIfExists(this, filePath)) { + return false; + } + model->convertToCockatriceFormat(row()); return true; - }); + } + + // Show the dialog to the user + DialogConvertDeckToCodFormat conversionDialog(this); + if (conversionDialog.exec() != QDialog::Accepted) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion( + !conversionDialog.dontAskAgain()); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(false); + + return false; + } + + // Try to convert file + if (!confirmOverwriteIfExists(this, filePath)) { + return false; + } + + model->convertToCockatriceFormat(row()); + + if (conversionDialog.dontAskAgain()) { + SettingsCache::instance().visualDeckStorage().setVisualDeckStoragePromptForConversion(false); + SettingsCache::instance().visualDeckStorage().setVisualDeckStorageAlwaysConvert(true); + } + + return true; } diff --git a/format.sh b/format.sh index ca3557ea7..3fa435be1 100755 --- a/format.sh +++ b/format.sh @@ -22,6 +22,7 @@ libcockatrice_* \ exclude=("libcockatrice_rng/libcockatrice/rng/sfmt/" \ "libcockatrice_utility/libcockatrice/utility/peglib.h" \ "oracle/src/lzma/" \ +"oracle/src/qt-json/" \ "oracle/src/zip/" \ "servatrice/src/smtp/") exts=("cpp" "h" "proto") diff --git a/oracle/src/oracleimporter.cpp b/oracle/src/oracleimporter.cpp index 897264987..c46324b44 100644 --- a/oracle/src/oracleimporter.cpp +++ b/oracle/src/oracleimporter.cpp @@ -46,10 +46,10 @@ static CardSet::Priority getSetPriority(const QString &setType, const QString &s bool OracleImporter::readSetsFromByteArray(QByteArray data) { - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(data, &error); - if (error.isError()) { - qDebug() << "error: RawJson::scanSetRanges():" << error.message; + QList ranges; + const RawJson::ScanError scanError = RawJson::scanSetRanges(data, ranges); + if (scanError.isError()) { + qDebug() << "error: RawJson::scanSetRanges():" << scanError.message; return false; } @@ -81,7 +81,7 @@ bool OracleImporter::readSetsFromByteArray(QByteArray data) setType = setType.trimmed(); } SetToDownload set(shortName, longName, priority, setType, releaseDate); - set.setRawRange(range); + set.setRawRange(range.start, range.length, range.cardCount); newSetList.append(set); } @@ -91,7 +91,7 @@ bool OracleImporter::readSetsFromByteArray(QByteArray data) return false; } allSets = newSetList; - rawSetsData = data; + rawSetsData = std::move(data); return true; } @@ -221,7 +221,7 @@ CardInfoPtr OracleImporter::addCard(QString name, return newCard; } -static QString getJsonString(const QJsonObject &obj, const QString &key) +static inline QString getJsonString(const QJsonObject &obj, const QString &key) { return obj.value(key).toString(); } @@ -465,8 +465,13 @@ int OracleImporter::importCardsFromSet(const CardSetPtr ¤tSet, const QJson return numCards; } -static FormatRulesNameMap buildDefaultMagicFormats() +FormatRulesNameMap OracleImporter::createDefaultMagicFormats() { + static FormatRulesNameMap cached; + if (!cached.isEmpty()) { + return cached; + } + // Predefined common exceptions CardCondition superTypeIsBasic; superTypeIsBasic.field = "type"; @@ -484,7 +489,8 @@ static FormatRulesNameMap buildDefaultMagicFormats() ExceptionRule mayContainAnyNumber; mayContainAnyNumber.conditions.append(anyNumberAllowed); - FormatRulesNameMap defaultFormatRulesNameMap; + // Map to store default rules + FormatRulesNameMap &defaultFormatRulesNameMap = cached; // ----------------- Helper lambda to create format ----------------- auto makeFormat = [&](const QString &name, int minDeck = 60, int maxDeck = -1, int maxSideboardSize = 15, @@ -529,12 +535,6 @@ static FormatRulesNameMap buildDefaultMagicFormats() return defaultFormatRulesNameMap; } -FormatRulesNameMap OracleImporter::createDefaultMagicFormats() -{ - static const FormatRulesNameMap cached = buildDefaultMagicFormats(); - return cached; -} - int OracleImporter::startImport() { static ICardSetPriorityController *noOpController = new NoopCardSetPriorityController(); @@ -542,7 +542,7 @@ int OracleImporter::startImport() // Pre-allocate cards hash to avoid rehashing during import int estimatedCards = 0; for (const SetToDownload &curSetToParse : allSets) { - estimatedCards += curSetToParse.getRawRange().cardCount; + estimatedCards += curSetToParse.getCardCount(); } cards.reserve(estimatedCards); @@ -563,8 +563,7 @@ int OracleImporter::startImport() // parse only this set's slice of the raw document so the whole JSON tree is // never kept in memory at once - const RawJson::SetRange &rawRange = curSetToParse.getRawRange(); - const QByteArray setBytes(rawSetsData.constData() + rawRange.start, rawRange.length); + const QByteArray setBytes(rawSetsData.constData() + curSetToParse.getRawStart(), curSetToParse.getRawLength()); QJsonParseError parseError; const QJsonDocument setDoc = QJsonDocument::fromJson(setBytes, &parseError); if (parseError.error != QJsonParseError::NoError) { @@ -607,4 +606,6 @@ void OracleImporter::clear() cards.clear(); allSets.clear(); rawSetsData.clear(); + // Note: createDefaultMagicFormats() uses a function-local static cache that is + // intentionally not cleared here since format rules are hardcoded constants. } diff --git a/oracle/src/oracleimporter.h b/oracle/src/oracleimporter.h index 1450e2d99..2234d9dc1 100644 --- a/oracle/src/oracleimporter.h +++ b/oracle/src/oracleimporter.h @@ -54,7 +54,9 @@ private: CardSet::Priority priority; // Byte range of this set's object within the importer's raw JSON text. Parsing // one set at a time keeps peak memory low instead of holding the whole document. - RawJson::SetRange rawRange; + qsizetype rawStart = -1; + qsizetype rawLength = 0; + int cardCount = 0; public: const QString &getShortName() const @@ -77,9 +79,17 @@ public: { return priority; } - const RawJson::SetRange &getRawRange() const + qsizetype getRawStart() const { - return rawRange; + return rawStart; + } + qsizetype getRawLength() const + { + return rawLength; + } + int getCardCount() const + { + return cardCount; } SetToDownload(QString _shortName, QString _longName, @@ -90,9 +100,11 @@ public: setType(std::move(_setType)), priority(_priority) { } - void setRawRange(const RawJson::SetRange &_rawRange) + void setRawRange(qsizetype _rawStart, qsizetype _rawLength, int _cardCount) { - rawRange = _rawRange; + rawStart = _rawStart; + rawLength = _rawLength; + cardCount = _cardCount; } bool operator<(const SetToDownload &set) const { diff --git a/oracle/src/qt-json/AUTHORS b/oracle/src/qt-json/AUTHORS new file mode 100644 index 000000000..29a85929f --- /dev/null +++ b/oracle/src/qt-json/AUTHORS @@ -0,0 +1,3 @@ +Eeli Reilin +Luis Gustavo S. Barreto +Stephen Kockentiedt diff --git a/oracle/src/qt-json/LICENSE b/oracle/src/qt-json/LICENSE new file mode 100644 index 000000000..3c42b515a --- /dev/null +++ b/oracle/src/qt-json/LICENSE @@ -0,0 +1,27 @@ +Copyright 2011 Eeli Reilin. All rights reserved. + +Redistribution and use in source and binary forms, with or without modification, +are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, + this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY ''AS IS'' AND ANY EXPRESS OR +IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO +EVENT SHALL EELI REILIN OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, +INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, +OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE +OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF +ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +The views and conclusions contained in the software and documentation +are those of the authors and should not be interpreted as representing +official policies, either expressed or implied, of Eeli Reilin. + diff --git a/oracle/src/qt-json/README b/oracle/src/qt-json/README new file mode 100644 index 000000000..b60c1599b --- /dev/null +++ b/oracle/src/qt-json/README @@ -0,0 +1,96 @@ +######################################################################## +1. INTRODUCTION + +The Json class is a simple class for parsing JSON data into a QVariant +hierarchies. Now, we can also reverse the process and serialize +QVariant hierarchies into valid JSON data. + + +######################################################################## +2. HOW TO USE + +The parser is really easy to use. Let's say we have the following +QString of JSON data: + +------------------------------------------------------------------------ +{ + "encoding" : "UTF-8", + "plug-ins" : [ + "python", + "c++", + "ruby" + ], + "indent" : { + "length" : 3, + "use_space" : true + } +} +------------------------------------------------------------------------ + +We would first call the parse-method: + +------------------------------------------------------------------------ +//Say that we're using the QtJson namespace +using namespace QtJson; +bool ok; +//json is a QString containing the JSON data +QVariantMap result = Json::parse(json, ok).toMap(); + +if(!ok) { + qFatal("An error occurred during parsing"); + exit(1); +} +------------------------------------------------------------------------ + +Assuming the parsing process completed without errors, we would then +go through the hierarchy: + +------------------------------------------------------------------------ +qDebug() << "encoding:" << result["encoding"].toString(); +qDebug() << "plugins:"; + +foreach(QVariant plugin, result["plug-ins"].toList()) { + qDebug() << "\t-" << plugin.toString(); +} + +QVariantMap nestedMap = result["indent"].toMap(); +qDebug() << "length:" << nestedMap["length"].toInt(); +qDebug() << "use_space:" << nestedMap["use_space"].toBool(); +------------------------------------------------------------------------ + +The previous code would print out the following: + +------------------------------------------------------------------------ +encoding: "UTF-8" +plugins: + - "python" + - "c++" + - "ruby" +length: 3 +use_space: true +------------------------------------------------------------------------ + +To write JSON data from Qt object is as simple as parsing: + +------------------------------------------------------------------------ +QVariantMap map; +map["name"] = "Name"; +map["age"] = 22; + +QByteArray data = Json::serialize(map); +------------------------------------------------------------------------ + +The byte array 'data' contains valid JSON data: + +------------------------------------------------------------------------ +{ + name: "Luis Gustavo", + age: 22, +} +------------------------------------------------------------------------ + + +######################################################################## +4. CONTRIBUTING + +The code is available to download at GitHub. Contribute if you dare! diff --git a/oracle/src/raw_json_scanner.cpp b/oracle/src/raw_json_scanner.cpp index 0311605f3..0faf7ec14 100644 --- a/oracle/src/raw_json_scanner.cpp +++ b/oracle/src/raw_json_scanner.cpp @@ -468,29 +468,19 @@ int countArrayElements(const char *p, const char *end) namespace RawJson { -QList scanSetRanges(const QByteArray &json, ScanError *error) +ScanError scanSetRanges(const QByteArray &json, QList &ranges) { - QList ranges; - if (error) { - *error = ScanError{}; - } - - const auto fail = [&](const QString &message) -> QList { - if (error) { - error->message = message; - } - return {}; - }; + ranges.clear(); const char *begin = json.constData(); const char *end = begin + json.size(); if (begin >= end) { - return fail(QStringLiteral("empty JSON document")); + return ScanError{"empty JSON document"}; } const char *p = skipWhitespace(begin, end); if (p >= end || *p != '{') { - return fail(QStringLiteral("top-level JSON must be an object")); + return ScanError{"top-level JSON must be an object"}; } bool foundData = false; @@ -555,19 +545,19 @@ QList scanSetRanges(const QByteArray &json, ScanError *error) }; if (!forEachObjectMember(p, end, topLevelCallback)) { - return fail(malformedSetData ? QStringLiteral("malformed set data") : QStringLiteral("malformed JSON")); + return ScanError{malformedSetData ? QStringLiteral("malformed set data") : QStringLiteral("malformed JSON")}; } p = skipWhitespace(p, end); if (p != end) { - return fail(QStringLiteral("trailing content after top-level JSON object")); + return ScanError{"trailing content after top-level JSON object"}; } if (!foundData) { - return fail(QStringLiteral("missing \"data\" object")); + return ScanError{"missing \"data\" object"}; } if (ranges.isEmpty()) { - return fail(QStringLiteral("no sets found in \"data\"")); + return ScanError{"no sets found in \"data\""}; } - return ranges; + return ScanError{}; } } // namespace RawJson \ No newline at end of file diff --git a/oracle/src/raw_json_scanner.h b/oracle/src/raw_json_scanner.h index f4c1ba0ab..c32a71951 100644 --- a/oracle/src/raw_json_scanner.h +++ b/oracle/src/raw_json_scanner.h @@ -42,18 +42,8 @@ struct ScanError * The whole document is structurally validated while scanning (strings, * escapes, braces, and a trailing-content check), so malformed input is * rejected just like QJsonDocument::fromJson would. - * - * Following QJsonDocument::fromJson's convention, the parsed ranges are - * returned by value and any failure is reported through the @p error out - * parameter. - * - * @param json The raw MTGJSON document bytes. - * @param error Out parameter. Set to an error ScanError when the document - * cannot be parsed, otherwise left empty. Passing a null - * pointer disables error reporting. - * @return The detected per-set ranges, or an empty list on failure. */ -QList scanSetRanges(const QByteArray &json, ScanError *error = nullptr); +ScanError scanSetRanges(const QByteArray &json, QList &ranges); } // namespace RawJson diff --git a/tests/oracle/oracle_importer_test.cpp b/tests/oracle/oracle_importer_test.cpp index 9f4b0ab7b..103c2e7e6 100644 --- a/tests/oracle/oracle_importer_test.cpp +++ b/tests/oracle/oracle_importer_test.cpp @@ -502,8 +502,8 @@ TEST_F(OracleImporterTest, ScanSetRangesMatchFullJsonParse) const QByteArray bytes = QJsonDocument(root).toJson(QJsonDocument::Compact); - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(bytes, &error); + QList ranges; + const RawJson::ScanError error = RawJson::scanSetRanges(bytes, ranges); ASSERT_FALSE(error.isError()) << error.message.toStdString(); ASSERT_EQ(ranges.size(), 2); @@ -524,9 +524,8 @@ TEST_F(OracleImporterTest, ScanSetRangesDecodesEscapesAndCountsCards) "\"type\":\"expansion\",\"releaseDate\":\"2024-01-05\"," "\"cards\":[{\"name\":\"a\"},{\"name\":\"b\"},{\"name\":\"c\"}]}}}"; - RawJson::ScanError error; - const QList ranges = RawJson::scanSetRanges(json, &error); - ASSERT_FALSE(error.isError()); + QList ranges; + ASSERT_FALSE(RawJson::scanSetRanges(json, ranges).isError()); ASSERT_EQ(ranges.size(), 1); const RawJson::SetRange &range = ranges.first(); @@ -559,8 +558,8 @@ TEST_F(OracleImporterTest, ScanSetRangesRejectsInvalidJson) "{\"data\":{\"A\":{\"cards\":[{\"name\":\"\\ud800\"}]}}}"}; for (const QByteArray &json : invalid) { - RawJson::ScanError error; - RawJson::scanSetRanges(json, &error); + QList ranges; + const RawJson::ScanError error = RawJson::scanSetRanges(json, ranges); EXPECT_TRUE(error.isError()) << "expected failure for: " << json.constData(); } }