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 <hjk@qt.io>
This commit is contained in:
Andreas Loth
2023-07-25 18:48:18 +02:00
parent 2ff5293c4a
commit 8f9e75a3b8
5 changed files with 98 additions and 241 deletions

View File

@@ -4,8 +4,8 @@
#include "axivionoutputpane.h" #include "axivionoutputpane.h"
#include "axivionplugin.h" #include "axivionplugin.h"
#include "axivionresultparser.h"
#include "axiviontr.h" #include "axiviontr.h"
#include "dashboard/dto.h"
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
@@ -18,6 +18,9 @@
#include <QTextBrowser> #include <QTextBrowser>
#include <QToolButton> #include <QToolButton>
#include <map>
#include <memory>
namespace Axivion::Internal { namespace Axivion::Internal {
class DashboardWidget : public QScrollArea class DashboardWidget : public QScrollArea
@@ -58,7 +61,7 @@ DashboardWidget::DashboardWidget(QWidget *parent)
setWidgetResizable(true); 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 unchanged = Utils::Icons::NEXT.pixmap();
static const QPixmap increased = Utils::Icon( static const QPixmap increased = Utils::Icon(
@@ -70,10 +73,20 @@ static QPixmap trendIcon(int added, int removed)
return added < removed ? decreased : increased; return added < removed ? decreased : increased;
} }
static qint64 extract_value(const std::map<QString, Dto::Any> &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<qint64>(value.getDouble());
}
void DashboardWidget::updateUi() void DashboardWidget::updateUi()
{ {
const ProjectInfo &info = AxivionPlugin::projectInfo(); m_project->setText({});
m_project->setText(info.name);
m_loc->setText({}); m_loc->setText({});
m_timestamp->setText({}); m_timestamp->setText({});
QLayoutItem *child; QLayoutItem *child;
@@ -81,60 +94,77 @@ void DashboardWidget::updateUi()
delete child->widget(); delete child->widget();
delete child; delete child;
} }
std::shared_ptr<const Dto::ProjectInfoDto> info = AxivionPlugin::projectInfo();
if (info.versions.isEmpty()) if (!info)
return;
m_project->setText(info->name);
if (info->versions.empty())
return; return;
const ResultVersion &last = info.versions.last(); const Dto::AnalysisVersionDto &last = info->versions.back();
m_loc->setText(QString::number(last.linesOfCode)); if (last.linesOfCode.has_value())
const QDateTime timeStamp = QDateTime::fromString(last.timeStamp, Qt::ISODate); m_loc->setText(QString::number(last.linesOfCode.value()));
m_timestamp->setText(timeStamp.isValid() ? timeStamp.toString("yyyy-MM-dd HH:mm:ss") 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")); : Tr::tr("unknown"));
const QList<IssueKind> &issueKinds = info.issueKinds; const std::vector<Dto::IssueKindInfoDto> &issueKinds = info->issueKinds;
auto toolTip = [issueKinds](const QString &prefix){ auto toolTip = [issueKinds](const QString &prefix){
for (const IssueKind &kind : issueKinds) { for (const Dto::IssueKindInfoDto &kind : issueKinds) {
if (kind.prefix == prefix) if (kind.prefix == prefix)
return kind.nicePlural; return kind.nicePluralName;
} }
return prefix; return prefix;
}; };
auto addValuesWidgets = [this, &toolTip](const IssueCount &issueCount, int row){ auto addValuesWidgets = [this, &toolTip](const QString &issueKind, qint64 total, qint64 added, qint64 removed, int row) {
const QString currentToolTip = toolTip(issueCount.issueKind); const QString currentToolTip = toolTip(issueKind);
QLabel *label = new QLabel(issueCount.issueKind, this); QLabel *label = new QLabel(issueKind, this);
label->setToolTip(currentToolTip); label->setToolTip(currentToolTip);
m_gridLayout->addWidget(label, row, 0); 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->setToolTip(currentToolTip);
label->setAlignment(Qt::AlignRight); label->setAlignment(Qt::AlignRight);
m_gridLayout->addWidget(label, row, 1); m_gridLayout->addWidget(label, row, 1);
label = new QLabel(this); label = new QLabel(this);
label->setPixmap(trendIcon(issueCount.added, issueCount.removed)); label->setPixmap(trendIcon(added, removed));
label->setToolTip(currentToolTip); label->setToolTip(currentToolTip);
m_gridLayout->addWidget(label, row, 2); m_gridLayout->addWidget(label, row, 2);
label = new QLabel('+' + QString::number(issueCount.added)); label = new QLabel('+' + QString::number(added));
label->setAlignment(Qt::AlignRight); label->setAlignment(Qt::AlignRight);
label->setToolTip(currentToolTip); label->setToolTip(currentToolTip);
m_gridLayout->addWidget(label, row, 3); m_gridLayout->addWidget(label, row, 3);
label = new QLabel("/"); label = new QLabel("/");
label->setToolTip(currentToolTip); label->setToolTip(currentToolTip);
m_gridLayout->addWidget(label, row, 4); m_gridLayout->addWidget(label, row, 4);
label = new QLabel('-' + QString::number(issueCount.removed)); label = new QLabel('-' + QString::number(removed));
label->setAlignment(Qt::AlignRight); label->setAlignment(Qt::AlignRight);
label->setToolTip(currentToolTip); label->setToolTip(currentToolTip);
m_gridLayout->addWidget(label, row, 5); m_gridLayout->addWidget(label, row, 5);
}; };
int allTotal = 0, allAdded = 0, allRemoved = 0, row = 0; qint64 allTotal = 0;
for (auto issueCount : std::as_const(last.issueCounts)) { qint64 allAdded = 0;
allTotal += issueCount.total; qint64 allRemoved = 0;
allAdded += issueCount.added; qint64 row = 0;
allRemoved += issueCount.removed; // This code is overly complex because of a heedlessness in the
addValuesWidgets(issueCount, row); // Axivion Dashboard API definition. Other Axivion IDE plugins do
++row; // 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;
}
}
} }
addValuesWidgets(Tr::tr("Total:"), allTotal, allAdded, allRemoved, row);
const IssueCount total{{}, Tr::tr("Total:"), allTotal, allAdded, allRemoved};
addValuesWidgets(total, row);
} }
AxivionOutputPane::AxivionOutputPane(QObject *parent) AxivionOutputPane::AxivionOutputPane(QObject *parent)

View File

@@ -8,6 +8,7 @@
#include "axivionquery.h" #include "axivionquery.h"
#include "axivionresultparser.h" #include "axivionresultparser.h"
#include "axiviontr.h" #include "axiviontr.h"
#include "dashboard/dto.h"
#include <coreplugin/editormanager/documentmodel.h> #include <coreplugin/editormanager/documentmodel.h>
#include <coreplugin/editormanager/editormanager.h> #include <coreplugin/editormanager/editormanager.h>
@@ -24,12 +25,16 @@
#include <texteditor/texteditor.h> #include <texteditor/texteditor.h>
#include <texteditor/textmark.h> #include <texteditor/textmark.h>
#include <utils/expected.h>
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include <QAction> #include <QAction>
#include <QTimer> #include <QTimer>
#include <exception>
#include <memory>
constexpr char AxivionTextMarkId[] = "AxivionTextMark"; constexpr char AxivionTextMarkId[] = "AxivionTextMark";
namespace Axivion::Internal { namespace Axivion::Internal {
@@ -39,7 +44,7 @@ class AxivionPluginPrivate : public QObject
public: public:
void onStartupProjectChanged(); void onStartupProjectChanged();
void fetchProjectInfo(const QString &projectName); void fetchProjectInfo(const QString &projectName);
void handleProjectInfo(const ProjectInfo &info); void handleProjectInfo(const QByteArray &result);
void handleOpenedDocs(ProjectExplorer::Project *project); void handleOpenedDocs(ProjectExplorer::Project *project);
void onDocumentOpened(Core::IDocument *doc); void onDocumentOpened(Core::IDocument *doc);
void onDocumentClosed(Core::IDocument * doc); void onDocumentClosed(Core::IDocument * doc);
@@ -48,7 +53,7 @@ public:
void fetchRuleInfo(const QString &id); void fetchRuleInfo(const QString &id);
AxivionOutputPane m_axivionOutputPane; AxivionOutputPane m_axivionOutputPane;
ProjectInfo m_currentProjectInfo; std::shared_ptr<const Dto::ProjectInfoDto> m_currentProjectInfo;
bool m_runningQuery = false; bool m_runningQuery = false;
}; };
@@ -113,7 +118,7 @@ void AxivionPlugin::fetchProjectInfo(const QString &projectName)
dd->fetchProjectInfo(projectName); dd->fetchProjectInfo(projectName);
} }
ProjectInfo AxivionPlugin::projectInfo() std::shared_ptr<const Dto::ProjectInfoDto> AxivionPlugin::projectInfo()
{ {
QTC_ASSERT(dd, return {}); QTC_ASSERT(dd, return {});
return dd->m_currentProjectInfo; return dd->m_currentProjectInfo;
@@ -124,7 +129,7 @@ void AxivionPluginPrivate::onStartupProjectChanged()
ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject(); ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject();
if (!project) { if (!project) {
clearAllMarks(); clearAllMarks();
m_currentProjectInfo = ProjectInfo(); m_currentProjectInfo = {};
m_axivionOutputPane.updateDashboard(); m_axivionOutputPane.updateDashboard();
return; return;
} }
@@ -141,7 +146,7 @@ void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName)
} }
clearAllMarks(); clearAllMarks();
if (projectName.isEmpty()) { if (projectName.isEmpty()) {
m_currentProjectInfo = ProjectInfo(); m_currentProjectInfo = {};
m_axivionOutputPane.updateDashboard(); m_axivionOutputPane.updateDashboard();
return; return;
} }
@@ -150,7 +155,7 @@ void AxivionPluginPrivate::fetchProjectInfo(const QString &projectName)
AxivionQuery query(AxivionQuery::ProjectInfo, {projectName}); AxivionQuery query(AxivionQuery::ProjectInfo, {projectName});
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this); AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){ connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){
handleProjectInfo(ResultParser::parseProjectInfo(result)); handleProjectInfo(result);
}); });
connect(runner, &AxivionQueryRunner::finished, [runner]{ runner->deleteLater(); }); connect(runner, &AxivionQueryRunner::finished, [runner]{ runner->deleteLater(); });
runner->start(); runner->start();
@@ -196,20 +201,16 @@ void AxivionPluginPrivate::clearAllMarks()
onDocumentClosed(doc); onDocumentClosed(doc);
} }
void AxivionPluginPrivate::handleProjectInfo(const ProjectInfo &info) void AxivionPluginPrivate::handleProjectInfo(const QByteArray &result)
{ {
Utils::expected_str<Dto::ProjectInfoDto> raw_info = ResultParser::parseProjectInfo(result);
m_runningQuery = false; m_runningQuery = false;
if (!info.error.isEmpty()) { if (!raw_info) {
Core::MessageManager::writeFlashing("Axivion: " + info.error); Core::MessageManager::writeFlashing(QStringLiteral(u"Axivion: ") + raw_info.error());
return; return;
} }
m_currentProjectInfo = std::make_shared<const Dto::ProjectInfoDto>(std::move(raw_info.value()));
m_currentProjectInfo = info;
m_axivionOutputPane.updateDashboard(); m_axivionOutputPane.updateDashboard();
if (m_currentProjectInfo.name.isEmpty())
return;
// handle already opened documents // handle already opened documents
if (auto buildSystem = ProjectExplorer::ProjectManager::startupBuildSystem(); if (auto buildSystem = ProjectExplorer::ProjectManager::startupBuildSystem();
!buildSystem || !buildSystem->isParsing()) { !buildSystem || !buildSystem->isParsing()) {
@@ -223,7 +224,7 @@ void AxivionPluginPrivate::handleProjectInfo(const ProjectInfo &info)
void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc) 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; return;
ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject(); ProjectExplorer::Project *project = ProjectExplorer::ProjectManager::startupProject();
@@ -232,7 +233,7 @@ void AxivionPluginPrivate::onDocumentOpened(Core::IDocument *doc)
Utils::FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory()); Utils::FilePath relative = doc->filePath().relativeChildPath(project->projectDirectory());
// for now only style violations // for now only style violations
AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo.name, "SV", AxivionQuery query(AxivionQuery::IssuesForFileList, {m_currentProjectInfo->name, "SV",
relative.path() } ); relative.path() } );
AxivionQueryRunner *runner = new AxivionQueryRunner(query, this); AxivionQueryRunner *runner = new AxivionQueryRunner(query, this);
connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){ connect(runner, &AxivionQueryRunner::resultRetrieved, this, [this](const QByteArray &result){

View File

@@ -3,8 +3,12 @@
#pragma once #pragma once
#include "dashboard/dto.h"
#include <extensionsystem/iplugin.h> #include <extensionsystem/iplugin.h>
#include <memory>
namespace ProjectExplorer { class Project; } namespace ProjectExplorer { class Project; }
namespace Axivion::Internal { namespace Axivion::Internal {
@@ -22,7 +26,7 @@ public:
~AxivionPlugin() final; ~AxivionPlugin() final;
static void fetchProjectInfo(const QString &projectName); static void fetchProjectInfo(const QString &projectName);
static ProjectInfo projectInfo(); static std::shared_ptr<const Dto::ProjectInfoDto> projectInfo();
private: private:
void initialize() final; void initialize() final;

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "axivionresultparser.h" #include "axivionresultparser.h"
#include "dashboard/dto.h"
#include <utils/qtcassert.h> #include <utils/qtcassert.h>
@@ -10,6 +11,7 @@
#include <QJsonObject> #include <QJsonObject>
#include <QRegularExpression> #include <QRegularExpression>
#include <stdexcept>
#include <utility> #include <utility>
namespace Axivion::Internal { namespace Axivion::Internal {
@@ -79,124 +81,6 @@ static std::pair<BaseResult, QJsonDocument> prehandleHeaderAndBody(const QByteAr
return {result, doc}; 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<User> usersFromJson(const QJsonArray &array)
{
QList<User> 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<IssueCount> issueCountsFromJson(const QJsonObject &object)
{
QList<IssueCount> 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<ResultVersion> versionsFromJson(const QJsonArray &array)
{
QList<ResultVersion> 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<IssueKind> issueKindsFromJson(const QJsonArray &array)
{
QList<IssueKind> 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 { namespace ResultParser {
DashboardInfo parseDashboardInfo(const QByteArray &input) DashboardInfo parseDashboardInfo(const QByteArray &input)
@@ -236,41 +120,18 @@ DashboardInfo parseDashboardInfo(const QByteArray &input)
return result; return result;
} }
ProjectInfo parseProjectInfo(const QByteArray &input) Utils::expected_str<Dto::ProjectInfoDto> parseProjectInfo(const QByteArray &input)
{ {
ProjectInfo result;
auto [header, body] = splitHeaderAndBody(input); auto [header, body] = splitHeaderAndBody(input);
auto [error, doc] = prehandleHeaderAndBody(header, body); auto [error, doc] = prehandleHeaderAndBody(header, body);
if (!error.error.isEmpty()) { if (!error.error.isEmpty())
result.error = error.error; return tl::make_unexpected(std::move(error.error));
return result; 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) static QRegularExpression issueCsvLineRegex(const QByteArray &firstCsvLine)

View File

@@ -3,6 +3,10 @@
#pragma once #pragma once
#include "dashboard/dto.h"
#include <utils/expected.h>
#include <QList> #include <QList>
namespace Axivion::Internal { namespace Axivion::Internal {
@@ -27,49 +31,6 @@ public:
QList<Project> projects; QList<Project> 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<IssueCount> issueCounts;
int linesOfCode = 0;
};
class ProjectInfo : public BaseResult
{
public:
QString name;
QList<User> users;
QList<ResultVersion> versions;
QList<IssueKind> issueKinds;
};
class ShortIssue : public BaseResult class ShortIssue : public BaseResult
{ {
public: public:
@@ -92,7 +53,7 @@ public:
namespace ResultParser { namespace ResultParser {
DashboardInfo parseDashboardInfo(const QByteArray &input); DashboardInfo parseDashboardInfo(const QByteArray &input);
ProjectInfo parseProjectInfo(const QByteArray &input); Utils::expected_str<Dto::ProjectInfoDto> parseProjectInfo(const QByteArray &input);
IssuesList parseIssuesList(const QByteArray &input); IssuesList parseIssuesList(const QByteArray &input);
QString parseRuleInfo(const QByteArray &input); QString parseRuleInfo(const QByteArray &input);