From 8f9e75a3b89f61995d233ebf4a062b796ebb2317 Mon Sep 17 00:00:00 2001 From: Andreas Loth Date: Tue, 25 Jul 2023 18:48:18 +0200 Subject: [PATCH] Axivion: Use DTO for ProjectInfo deserialization This is a proof of concept that the DTOs can be used to deserialize and access the data the Axivion Dashboard returns. In future, this is going to replace all manual deserialization and manually written Dashboard interface classes. Change-Id: Ic3c997f6aca7fcb179cd19fa9b5107fe7d4dec03 Reviewed-by: hjk --- src/plugins/axivion/axivionoutputpane.cpp | 90 +++++++---- src/plugins/axivion/axivionplugin.cpp | 35 ++--- src/plugins/axivion/axivionplugin.h | 6 +- src/plugins/axivion/axivionresultparser.cpp | 159 ++------------------ src/plugins/axivion/axivionresultparser.h | 49 +----- 5 files changed, 98 insertions(+), 241 deletions(-) diff --git a/src/plugins/axivion/axivionoutputpane.cpp b/src/plugins/axivion/axivionoutputpane.cpp index c2443223cdd..4e8326da0b5 100644 --- a/src/plugins/axivion/axivionoutputpane.cpp +++ b/src/plugins/axivion/axivionoutputpane.cpp @@ -4,8 +4,8 @@ #include "axivionoutputpane.h" #include "axivionplugin.h" -#include "axivionresultparser.h" #include "axiviontr.h" +#include "dashboard/dto.h" #include #include @@ -18,6 +18,9 @@ #include #include +#include +#include + namespace Axivion::Internal { class DashboardWidget : public QScrollArea @@ -58,7 +61,7 @@ DashboardWidget::DashboardWidget(QWidget *parent) setWidgetResizable(true); } -static QPixmap trendIcon(int added, int removed) +static QPixmap trendIcon(qint64 added, qint64 removed) { static const QPixmap unchanged = Utils::Icons::NEXT.pixmap(); static const QPixmap increased = Utils::Icon( @@ -70,10 +73,20 @@ static QPixmap trendIcon(int added, int removed) return added < removed ? decreased : increased; } +static qint64 extract_value(const std::map &map, const QString &key) +{ + const auto search = map.find(key); + if (search == map.end()) + return 0; + const Dto::Any &value = search->second; + if (!value.isDouble()) + return 0; + return static_cast(value.getDouble()); +} + void DashboardWidget::updateUi() { - const ProjectInfo &info = AxivionPlugin::projectInfo(); - m_project->setText(info.name); + m_project->setText({}); m_loc->setText({}); m_timestamp->setText({}); QLayoutItem *child; @@ -81,60 +94,77 @@ void DashboardWidget::updateUi() delete child->widget(); delete child; } - - if (info.versions.isEmpty()) + std::shared_ptr info = AxivionPlugin::projectInfo(); + if (!info) + return; + m_project->setText(info->name); + if (info->versions.empty()) return; - const ResultVersion &last = info.versions.last(); - m_loc->setText(QString::number(last.linesOfCode)); - const QDateTime timeStamp = QDateTime::fromString(last.timeStamp, Qt::ISODate); - m_timestamp->setText(timeStamp.isValid() ? timeStamp.toString("yyyy-MM-dd HH:mm:ss") + const Dto::AnalysisVersionDto &last = info->versions.back(); + if (last.linesOfCode.has_value()) + m_loc->setText(QString::number(last.linesOfCode.value())); + const QDateTime timeStamp = QDateTime::fromString(last.date, Qt::ISODate); + m_timestamp->setText(timeStamp.isValid() ? timeStamp.toString("yyyy-MM-dd HH:mm:ss t") : Tr::tr("unknown")); - const QList &issueKinds = info.issueKinds; + const std::vector &issueKinds = info->issueKinds; auto toolTip = [issueKinds](const QString &prefix){ - for (const IssueKind &kind : issueKinds) { + for (const Dto::IssueKindInfoDto &kind : issueKinds) { if (kind.prefix == prefix) - return kind.nicePlural; + return kind.nicePluralName; } return prefix; }; - auto addValuesWidgets = [this, &toolTip](const IssueCount &issueCount, int row){ - const QString currentToolTip = toolTip(issueCount.issueKind); - QLabel *label = new QLabel(issueCount.issueKind, this); + auto addValuesWidgets = [this, &toolTip](const QString &issueKind, qint64 total, qint64 added, qint64 removed, int row) { + const QString currentToolTip = toolTip(issueKind); + QLabel *label = new QLabel(issueKind, this); label->setToolTip(currentToolTip); m_gridLayout->addWidget(label, row, 0); - label = new QLabel(QString::number(issueCount.total), this); + label = new QLabel(QString::number(total), this); label->setToolTip(currentToolTip); label->setAlignment(Qt::AlignRight); m_gridLayout->addWidget(label, row, 1); label = new QLabel(this); - label->setPixmap(trendIcon(issueCount.added, issueCount.removed)); + label->setPixmap(trendIcon(added, removed)); label->setToolTip(currentToolTip); m_gridLayout->addWidget(label, row, 2); - label = new QLabel('+' + QString::number(issueCount.added)); + label = new QLabel('+' + QString::number(added)); label->setAlignment(Qt::AlignRight); label->setToolTip(currentToolTip); m_gridLayout->addWidget(label, row, 3); label = new QLabel("/"); label->setToolTip(currentToolTip); m_gridLayout->addWidget(label, row, 4); - label = new QLabel('-' + QString::number(issueCount.removed)); + label = new QLabel('-' + QString::number(removed)); label->setAlignment(Qt::AlignRight); label->setToolTip(currentToolTip); m_gridLayout->addWidget(label, row, 5); }; - int allTotal = 0, allAdded = 0, allRemoved = 0, row = 0; - for (auto issueCount : std::as_const(last.issueCounts)) { - allTotal += issueCount.total; - allAdded += issueCount.added; - allRemoved += issueCount.removed; - addValuesWidgets(issueCount, row); - ++row; + qint64 allTotal = 0; + qint64 allAdded = 0; + qint64 allRemoved = 0; + qint64 row = 0; + // This code is overly complex because of a heedlessness in the + // Axivion Dashboard API definition. Other Axivion IDE plugins do + // not use the issue counts, thus the QtCreator Axivion Plugin + // is going to stop using them, too. + if (last.issueCounts.isMap()) { + for (const auto &issueCount : last.issueCounts.getMap()) { + if (issueCount.second.isMap()) { + const auto &counts = issueCount.second.getMap(); + qint64 total = extract_value(counts, QStringLiteral(u"Total")); + allTotal += total; + qint64 added = extract_value(counts, QStringLiteral(u"Added")); + allAdded += added; + qint64 removed = extract_value(counts, QStringLiteral(u"Removed")); + allRemoved += removed; + addValuesWidgets(issueCount.first, total, added, removed, row); + ++row; + } + } } - - const IssueCount total{{}, Tr::tr("Total:"), allTotal, allAdded, allRemoved}; - addValuesWidgets(total, row); + addValuesWidgets(Tr::tr("Total:"), allTotal, allAdded, allRemoved, row); } AxivionOutputPane::AxivionOutputPane(QObject *parent) diff --git a/src/plugins/axivion/axivionplugin.cpp b/src/plugins/axivion/axivionplugin.cpp index ef253e9c0d5..3d89c8ba957 100644 --- a/src/plugins/axivion/axivionplugin.cpp +++ b/src/plugins/axivion/axivionplugin.cpp @@ -8,6 +8,7 @@ #include "axivionquery.h" #include "axivionresultparser.h" #include "axiviontr.h" +#include "dashboard/dto.h" #include #include @@ -24,12 +25,16 @@ #include #include +#include #include #include #include #include +#include +#include + constexpr char AxivionTextMarkId[] = "AxivionTextMark"; namespace Axivion::Internal { @@ -39,7 +44,7 @@ class AxivionPluginPrivate : public QObject public: void onStartupProjectChanged(); void fetchProjectInfo(const QString &projectName); - void handleProjectInfo(const ProjectInfo &info); + void handleProjectInfo(const QByteArray &result); void handleOpenedDocs(ProjectExplorer::Project *project); void onDocumentOpened(Core::IDocument *doc); void onDocumentClosed(Core::IDocument * doc); @@ -48,7 +53,7 @@ public: void fetchRuleInfo(const QString &id); AxivionOutputPane m_axivionOutputPane; - ProjectInfo m_currentProjectInfo; + std::shared_ptr m_currentProjectInfo; bool m_runningQuery = false; }; @@ -113,7 +118,7 @@ void AxivionPlugin::fetchProjectInfo(const QString &projectName) dd->fetchProjectInfo(projectName); } -ProjectInfo AxivionPlugin::projectInfo() +std::shared_ptr AxivionPlugin::projectInfo() { QTC_ASSERT(dd, return {}); return dd->m_currentProjectInfo; @@ -124,7 +129,7 @@ void AxivionPluginPrivate::onStartupProjectChanged() ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject(); if (!project) { clearAllMarks(); - m_currentProjectInfo = ProjectInfo(); + m_currentProjectInfo = {}; m_axivionOutputPane.updateDashboard(); return; } @@ -141,7 +146,7 @@ void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName) } clearAllMarks(); if (projectName.isEmpty()) { - m_currentProjectInfo = ProjectInfo(); + m_currentProjectInfo = {}; m_axivionOutputPane.updateDashboard(); return; } @@ -150,7 +155,7 @@ void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName) AxivionQuery query(AxivionQuery::ProjectInfo, {projectName}); AxivionQueryRunner *runner = new AxivionQueryRunner(query, this); connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){ - handleProjectInfo(ResultParser::parseProjectInfo(result)); + handleProjectInfo(result); }); connect(runner, &AxivionQueryRunner::finished, [runner]{ runner->deleteLater(); }); runner->start(); @@ -196,20 +201,16 @@ void AxivionPluginPrivate::clearAllMarks() onDocumentClosed(doc); } -void AxivionPluginPrivate::handleProjectInfo(const ProjectInfo &info) +void AxivionPluginPrivate::handleProjectInfo(const QByteArray &result) { + Utils::expected_str raw_info = ResultParser::parseProjectInfo(result); m_runningQuery = false; - if (!info.error.isEmpty()) { - Core::MessageManager::writeFlashing("Axivion: " + info.error); + if (!raw_info) { + Core::MessageManager::writeFlashing(QStringLiteral(u"Axivion: ") + raw_info.error()); return; } - - m_currentProjectInfo = info; + m_currentProjectInfo = std::make_shared(std::move(raw_info.value())); m_axivionOutputPane.updateDashboard(); - - if (m_currentProjectInfo.name.isEmpty()) - return; - // handle already opened documents if (auto buildSystem = ProjectExplorer::ProjectManager::startupBuildSystem(); !buildSystem || !buildSystem->isParsing()) { @@ -223,7 +224,7 @@ void AxivionPluginPrivate::handleProjectInfo(const ProjectInfo &info) void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc) { - if (m_currentProjectInfo.name.isEmpty()) // we do not have a project info (yet) + if (!m_currentProjectInfo) // we do not have a project info (yet) return; ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject(); @@ -232,7 +233,7 @@ void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc) Utils::FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory()); // for now only style violations - AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo.name, "SV", + AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo->name, "SV", relative.path() } ); AxivionQueryRunner *runner = new AxivionQueryRunner(query, this); connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){ diff --git a/src/plugins/axivion/axivionplugin.h b/src/plugins/axivion/axivionplugin.h index 9d71366fc45..ff235d393ce 100644 --- a/src/plugins/axivion/axivionplugin.h +++ b/src/plugins/axivion/axivionplugin.h @@ -3,8 +3,12 @@ #pragma once +#include "dashboard/dto.h" + #include +#include + namespace ProjectExplorer { class Project; } namespace Axivion::Internal { @@ -22,7 +26,7 @@ public: ~AxivionPlugin() final; static void fetchProjectInfo(const QString &projectName); - static ProjectInfo projectInfo(); + static std::shared_ptr projectInfo(); private: void initialize() final; diff --git a/src/plugins/axivion/axivionresultparser.cpp b/src/plugins/axivion/axivionresultparser.cpp index 0d8bb27b7db..81ae9787170 100644 --- a/src/plugins/axivion/axivionresultparser.cpp +++ b/src/plugins/axivion/axivionresultparser.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "axivionresultparser.h" +#include "dashboard/dto.h" #include @@ -10,6 +11,7 @@ #include #include +#include #include namespace Axivion::Internal { @@ -79,124 +81,6 @@ static std::pair prehandleHeaderAndBody(const QByteAr return {result, doc}; } -static User::UserType userTypeForString(const QString &type) -{ - if (type == "DASHBOARD_USER") - return User::Dashboard; - if (type == "VIRTUAL_USER") - return User::Virtual; - return User::Unknown; -} - -static User userFromJson(const QJsonObject &object) -{ - User result; - if (object.isEmpty()) { - result.error = "Not a user object."; - return result; - } - result.name = object.value("name").toString(); - result.displayName = object.value("displayName").toString(); - result.type = userTypeForString(object.value("type").toString()); - return result; -} - -static QList usersFromJson(const QJsonArray &array) -{ - QList result; - for (const QJsonValue &value : array) { - User user = userFromJson(value.toObject()); - if (!user.error.isEmpty()) // add this error to result.error? - continue; - result.append(user); - } - return result; -} - -static IssueCount issueCountFromJson(const QJsonObject &object) -{ - IssueCount result; - if (object.isEmpty()) { - result.error = "Not an issue count object."; - return result; - } - result.added = object.value("Added").toInt(); - result.removed = object.value("Removed").toInt(); - result.total = object.value("Total").toInt(); - return result; -} - -static QList issueCountsFromJson(const QJsonObject &object) -{ - QList result; - - const QStringList keys = object.keys(); - for (const QString &k : keys) { - IssueCount issue = issueCountFromJson(object.value(k).toObject()); - if (!issue.error.isEmpty()) // add this error to result.error? - continue; - issue.issueKind = k; - result.append(issue); - } - return result; -} - -static ResultVersion versionFromJson(const QJsonObject &object) -{ - ResultVersion result; - if (object.isEmpty()) { - result.error = "Not a version object."; - return result; - } - const QJsonValue issuesValue = object.value("issueCounts"); - if (!issuesValue.isObject()) { - result.error = "Not an object (issueCounts)."; - return result; - } - result.issueCounts = issueCountsFromJson(issuesValue.toObject()); - result.timeStamp = object.value("date").toString(); - result.name = object.value("name").toString(); - result.linesOfCode = object.value("linesOfCode").toInt(); - return result; -} - -static QList versionsFromJson(const QJsonArray &array) -{ - QList result; - for (const QJsonValue &value : array) { - ResultVersion version = versionFromJson(value.toObject()); - if (!version.error.isEmpty()) // add this error to result.error? - continue; - result.append(version); - } - return result; -} - -static IssueKind issueKindFromJson(const QJsonObject &object) -{ - IssueKind result; - if (object.isEmpty()) { - result.error = "Not an issue kind object."; - return result; - } - result.prefix = object.value("prefix").toString(); - result.niceSingular = object.value("niceSingularName").toString(); - result.nicePlural = object.value("nicePluralName").toString(); - return result; -} - -static QList issueKindsFromJson(const QJsonArray &array) -{ - QList result; - for (const QJsonValue &value : array) { - IssueKind kind = issueKindFromJson(value.toObject()); - if (!kind.error.isEmpty()) // add this error to result.error? - continue; - result.append(kind); - } - return result; -} - namespace ResultParser { DashboardInfo parseDashboardInfo(const QByteArray &input) @@ -236,41 +120,18 @@ DashboardInfo parseDashboardInfo(const QByteArray &input) return result; } -ProjectInfo parseProjectInfo(const QByteArray &input) +Utils::expected_str parseProjectInfo(const QByteArray &input) { - ProjectInfo result; - auto [header, body] = splitHeaderAndBody(input); auto [error, doc] = prehandleHeaderAndBody(header, body); - if (!error.error.isEmpty()) { - result.error = error.error; - return result; + if (!error.error.isEmpty()) + return tl::make_unexpected(std::move(error.error)); + try + { + return { Dto::ProjectInfoDto::deserialize(body) }; + } catch (const Dto::invalid_dto_exception &e) { + return tl::make_unexpected(QString::fromUtf8(e.what())); } - - const QJsonObject object = doc.object(); - result.name = object.value("name").toString(); - - const QJsonValue usersValue = object.value("users"); - if (!usersValue.isArray()) { - result.error = "Malformed json response (users)."; - return result; - } - result.users = usersFromJson(usersValue.toArray()); - - const QJsonValue versionsValue = object.value("versions"); - if (!versionsValue.isArray()) { - result.error = "Malformed json response (versions)."; - return result; - } - result.versions = versionsFromJson(versionsValue.toArray()); - - const QJsonValue issueKindsValue = object.value("issueKinds"); - if (!issueKindsValue.isArray()) { - result.error = "Malformed json response (issueKinds)."; - return result; - } - result.issueKinds = issueKindsFromJson(issueKindsValue.toArray()); - return result; } static QRegularExpression issueCsvLineRegex(const QByteArray &firstCsvLine) diff --git a/src/plugins/axivion/axivionresultparser.h b/src/plugins/axivion/axivionresultparser.h index 84c5a6b8ca8..d8d4a39a72c 100644 --- a/src/plugins/axivion/axivionresultparser.h +++ b/src/plugins/axivion/axivionresultparser.h @@ -3,6 +3,10 @@ #pragma once +#include "dashboard/dto.h" + +#include + #include namespace Axivion::Internal { @@ -27,49 +31,6 @@ public: QList projects; }; -class User : public BaseResult -{ -public: - QString name; - QString displayName; - enum UserType { Dashboard, Virtual, Unknown } type; -}; - -class IssueKind : public BaseResult -{ -public: - QString prefix; - QString niceSingular; - QString nicePlural; -}; - -class IssueCount : public BaseResult -{ -public: - QString issueKind; - int total = 0; - int added = 0; - int removed = 0; -}; - -class ResultVersion : public BaseResult -{ -public: - QString name; - QString timeStamp; - QList issueCounts; - int linesOfCode = 0; -}; - -class ProjectInfo : public BaseResult -{ -public: - QString name; - QList users; - QList versions; - QList issueKinds; -}; - class ShortIssue : public BaseResult { public: @@ -92,7 +53,7 @@ public: namespace ResultParser { DashboardInfo parseDashboardInfo(const QByteArray &input); -ProjectInfo parseProjectInfo(const QByteArray &input); +Utils::expected_str parseProjectInfo(const QByteArray &input); IssuesList parseIssuesList(const QByteArray &input); QString parseRuleInfo(const QByteArray &input);