big refactoring of API system

This commit is contained in:
Blue 2024-01-20 18:17:21 -03:00
parent 27124380e4
commit 7a116bfdf2
Signed by: blue
GPG key ID: 9B203B252A63EE38
37 changed files with 1060 additions and 534 deletions

View file

@ -0,0 +1,26 @@
# SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
# SPDX-License-Identifier: GPL-3.0-or-later
set(HEADERS
request.h
test.h
post.h
register.h
login.h
poll.h
listassets.h
addasset.h
)
set(SOURCES
request.cpp
test.cpp
post.cpp
register.cpp
login.cpp
poll.cpp
listassets.cpp
addasset.cpp
)
target_sources(magpie PRIVATE ${SOURCES})

10
API/requests/addasset.cpp Normal file
View file

@ -0,0 +1,10 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "addasset.h"
Request::AddAsset::AddAsset (const QString& title, const QString& icon, const QColor& color, unsigned int currency, const QUrl& baseUrl):
Post(createUrl(baseUrl, "/addAsset"), {{"title", title}, {"icon", icon}, {"currency", std::to_string(currency).c_str()}, {"color", "0"}})
{
emptyResult = true;
}

19
API/requests/addasset.h Normal file
View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <QColor>
#include "post.h"
namespace Request {
class AddAsset : public Post {
Q_OBJECT
public:
AddAsset (const QString& title, const QString& icon, const QColor& color, unsigned int currency, const QUrl& baseUrl);
};
}

View file

@ -0,0 +1,18 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "listassets.h"
#include "API/helpers.h"
Request::ListAssets::ListAssets (const QUrl& baseUrl):
Request(createUrl(baseUrl, "/listAssets")) {}
void Request::ListAssets::onSuccess (const QVariantMap& data) {
QVariantMap::ConstIterator itr = data.find("assets");
if (itr == data.constEnd() || !itr->canConvert<QVariantList>())
return Request::onError("Error receiving assets: assets are missing or not in an array", std::nullopt);
emit success(qast<QVariantList>(itr.value()));
emit done();
}

23
API/requests/listassets.h Normal file
View file

@ -0,0 +1,23 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "request.h"
namespace Request {
class ListAssets : public Request {
Q_OBJECT
public:
ListAssets (const QUrl& baseUrl);
signals:
void success(const QVariantList& assets);
protected:
void onSuccess (const QVariantMap& data) override;
};
}

34
API/requests/login.cpp Normal file
View file

@ -0,0 +1,34 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "login.h"
#include "API/codes.h"
const std::map<QString, QMetaType::Type> Request::Login::tokensStructure({
{"accessToken", QMetaType::QString}, {"renewToken", QMetaType::QString}
});
Request::Login::Login (const QString& login, const QString& password, const QUrl& baseUrl):
Post(createUrl(baseUrl, "/login"), {{"login", login}, {"password", password}}) {}
void Request::Login::onSuccess (const QVariantMap& data) {
if (!validateResponse(data, resultStructure))
return Request::onError("Malformed response", std::nullopt);
if (!validateResponse(data, tokensStructure))
return Request::onError("Malformed result: missing tokens", std::nullopt);
Codes::Login code = Codes::convertLogin(data.value("result").toInt());
if (code != Codes::Login::success)
return Request::onError("Failed to login: " + Codes::description(code), data);
Request::onSuccess(data);
}
void Request::Login::onError (const QString& err, const std::optional<QVariantMap>& data) {
if (validateResponse(data, resultStructure))
Request::onError(err + ": " + Codes::description(Codes::convertLogin(data->value("result").toInt())), data);
else
Request::onError(err, data);
}

23
API/requests/login.h Normal file
View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "post.h"
namespace Request {
class Login : public Post {
Q_OBJECT
public:
Login (const QString& login, const QString& password, const QUrl& baseUrl);
protected:
void onSuccess (const QVariantMap& data) override;
void onError (const QString& error, const std::optional<QVariantMap>& data) override;
static const std::map<QString, QMetaType::Type> tokensStructure;
};
}

46
API/requests/poll.cpp Normal file
View file

@ -0,0 +1,46 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "poll.h"
#include <QUrlQuery>
#include "API/codes.h"
const std::map<QString, QMetaType::Type> Request::Poll::updatesStructure({
{"data", QMetaType::QVariantMap},
});
Request::Poll::Poll (const QUrl& baseUrl, bool clear):
Request(createUrl(baseUrl, "/poll", clear))
{
request.setTransferTimeout(30000);
}
void Request::Poll::onSuccess (const QVariantMap& data) {
if (!validateResponse(data, resultStructure))
return Request::onError("Malformed response", std::nullopt);
Codes::Poll code = Codes::convertPoll(data.value("result").toInt());
if (code == Codes::Poll::success)
if (!validateResponse(data, updatesStructure))
return onError("Malformed response: received a poll that claimed to have some updates, but the \"data\" field is abscent", data);
Request::onSuccess(data);
}
void Request::Poll::onError (const QString& err, const std::optional<QVariantMap>& data) {
if (validateResponse(data, resultStructure))
Request::onError(err + ": " + Codes::description(Codes::convertPoll(data->value("result").toInt())), data);
else
Request::onError(err, data);
}
QUrl Request::Poll::createUrl (QUrl base, const QString& path, bool clear) {
QString startingPath = base.path();
base.setPath(startingPath + path);
if (clear)
base.setQuery(QUrlQuery({{"clearCache", "all"}}));
return base;
}

25
API/requests/poll.h Normal file
View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "request.h"
namespace Request {
class Poll : public Request {
Q_OBJECT
public:
Poll (const QUrl& baseUrl, bool clear = false);
protected:
void onSuccess (const QVariantMap& data) override;
void onError (const QString& error, const std::optional<QVariantMap>& data) override;
static QUrl createUrl(QUrl base, const QString& path, bool clear);
static const std::map<QString, QMetaType::Type> updatesStructure;
};
}

17
API/requests/post.cpp Normal file
View file

@ -0,0 +1,17 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "post.h"
Request::Post::Post (const QUrl& url, const QUrlQuery& form):
Request(url),
form(form)
{}
void Request::Post::aquireRequest (QNetworkAccessManager& manager) {
reply = std::unique_ptr<QNetworkReply, NetworkReplyDeleter>(manager.post(request, form.toString(QUrl::FullyEncoded).toUtf8()));
}
void Request::Post::initializeRequest () {
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
}

25
API/requests/post.h Normal file
View file

@ -0,0 +1,25 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "request.h"
#include <QUrlQuery>
namespace Request {
class Post : public Request {
Q_OBJECT
public:
Post(const QUrl& url, const QUrlQuery& form);
protected:
void aquireRequest (QNetworkAccessManager &manager) override;
void initializeRequest () override;
protected:
QUrlQuery form;
};
}

29
API/requests/register.cpp Normal file
View file

@ -0,0 +1,29 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "register.h"
#include <QUrlQuery>
#include "API/codes.h"
Request::Register::Register (const QString& login, const QString& password, const QUrl& baseUrl):
Post(createUrl(baseUrl, "/register"), {{"login", login}, {"password", password}}) {}
void Request::Register::onSuccess (const QVariantMap& data) {
if (!validateResponse(data, resultStructure))
return Request::onError("Malformed response", std::nullopt);
Codes::Register code = Codes::convertRegister(data.value("result").toInt());
if (code != Codes::Register::success)
return Request::onError("Failed to register: " + Codes::description(code), data);
Request::onSuccess(data);
}
void Request::Register::onError (const QString& err, const std::optional<QVariantMap>& data) {
if (validateResponse(data, resultStructure))
Request::onError(err + ": " + Codes::description(Codes::convertRegister(data->value("result").toInt())), data);
else
Request::onError(err, data);
}

21
API/requests/register.h Normal file
View file

@ -0,0 +1,21 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "post.h"
namespace Request {
class Register : public Post {
Q_OBJECT
public:
Register (const QString& login, const QString& password, const QUrl& baseUrl);
protected:
void onSuccess (const QVariantMap& data) override;
void onError (const QString& error, const std::optional<QVariantMap>& data) override;
};
}

134
API/requests/request.cpp Normal file
View file

@ -0,0 +1,134 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "request.h"
#include <QJsonDocument>
#include <QJsonObject>
const std::map<QString, QMetaType::Type> Request::Request::resultStructure({
{"result", QMetaType::LongLong},
});
Request::Request::Request (const QUrl& url):
state(State::initial),
request(url),
reply(),
emptyResult(false)
{}
Request::Request::~Request () {
cancel();
}
void Request::Request::send (QNetworkAccessManager& manager) {
if (state != State::initial)
throw new std::runtime_error("An attempt to send a request in a wrong state");
initializeRequest();
aquireRequest(manager);
connect(reply.get(), &QNetworkReply::finished, this, &Request::onFinished, Qt::QueuedConnection);
state = State::sent;
}
void Request::Request::cancel () {
if (stateTerminal())
return;
state = State::cancelled;
if (reply)
reply->abort();
}
void Request::Request::setAuthorizationToken (const QString& token) {
request.setRawHeader("Authorization", "Bearer " + token.toUtf8());
}
void Request::Request::aquireRequest (QNetworkAccessManager& manager) {
reply = std::unique_ptr<QNetworkReply, NetworkReplyDeleter>(manager.get(request));
}
void Request::Request::initializeRequest () {
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
}
std::optional<QString> Request::Request::validate () {
QNetworkReply::NetworkError error = reply->error();
if (error != QNetworkReply::NoError)
return reply->errorString();
else
return std::nullopt;
}
std::optional<QVariantMap> Request::Request::readResult () {
QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader);
if (!contentType.isValid() || !contentType.canConvert<QString>() || contentType.toString() != json)
return std::nullopt;
QByteArray data = reply->readAll();
QJsonDocument document = QJsonDocument::fromJson(data);
if (!document.isObject())
return std::nullopt;
QJsonObject object = document.object();
return object.toVariantMap();
}
void Request::Request::onSuccess (const QVariantMap& data) {
emit success(data);
emit done();
}
void Request::Request::onError (const QString& err, const std::optional<QVariantMap>& data) {
emit error(err, data);
emit done();
}
bool Request::Request::validateResponse (const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure) {
if (!data.has_value())
return false;
for (const std::pair<const QString, QMetaType::Type>& pair : structure) {
QVariantMap::ConstIterator itr = data->find(pair.first);
if (itr == data->end())
return false;
if (itr->userType() != pair.second)
return false;
}
return true;
}
QUrl Request::Request::createUrl (QUrl base, const QString& path) {
QString startingPath = base.path();
base.setPath(startingPath + path);
return base;
}
bool Request::Request::stateTerminal () const {
return state == State::responded || state == State::cancelled;
}
void Request::Request::onFinished () {
if (state != State::sent)
return;
state = State::responded;
std::optional<QString> err = validate();
std::optional<QVariantMap> data;
if (!emptyResult)
data = readResult();
if (err)
return onError(err.value(), data);
if (emptyResult)
return onSuccess({});
if (data)
return onSuccess(data.value());
else
return onError("Error reading request result", data);
}

69
API/requests/request.h Normal file
View file

@ -0,0 +1,69 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <memory>
#include <optional>
#include <QObject>
#include <QString>
#include <QVariantMap>
#include <QNetworkReply>
namespace Request {
class Request : public QObject {
Q_OBJECT
public:
Request (const QUrl& url);
virtual ~Request ();
void send (QNetworkAccessManager& manager);
void cancel ();
void setAuthorizationToken(const QString& token);
signals:
void done ();
void success (const QVariantMap& data);
void error (const QString& error, const std::optional<QVariantMap>& data);
protected:
virtual void aquireRequest (QNetworkAccessManager& manager);
virtual void initializeRequest ();
virtual void onSuccess (const QVariantMap& data);
virtual void onError (const QString& error, const std::optional<QVariantMap>& data);
std::optional<QString> validate ();
std::optional<QVariantMap> readResult ();
static bool validateResponse (const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure);
static QUrl createUrl(QUrl base, const QString& path);
bool stateTerminal() const;
constexpr static const char * const json = "application/json";
constexpr static const char * const urlEncoded = "application/x-www-form-urlencoded";
private slots:
void onFinished ();
protected:
static const std::map<QString, QMetaType::Type> resultStructure;
enum class State {
initial,
sent,
responded,
cancelled
};
struct NetworkReplyDeleter {
void operator () (QNetworkReply* reply) {
reply->deleteLater();
}
};
State state;
QNetworkRequest request;
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> reply;
bool emptyResult;
};
}

27
API/requests/test.cpp Normal file
View file

@ -0,0 +1,27 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "test.h"
const std::map<QString, QMetaType::Type> Request::Test::structure({
{"type", QMetaType::QString}, {"version", QMetaType::QString},
});
Request::Test::Test (const QString& path):
Request(path + "/test")
{}
void Request::Test::onSuccess (const QVariantMap& data) {
if (!validateResponse(data, structure))
return onError("Malformed response", data);
QString type = data.value("type").toString();
if (type != "pica")
return onError("server of this type (" + type + ") is not supported", data);
QString version = data.value("version").toString();
if (version != "0.0.1")
return onError("server of this version (" + version + ") is not supported", data);
Request::onSuccess(data);
}

23
API/requests/test.h Normal file
View file

@ -0,0 +1,23 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include "request.h"
namespace Request {
class Test : public Request {
Q_OBJECT
public:
Test (const QString& path);
protected:
void onSuccess (const QVariantMap &data) override;
private:
static const std::map<QString, QMetaType::Type> structure;
};
}