big refactoring of API system
This commit is contained in:
parent
27124380e4
commit
7a116bfdf2
37 changed files with 1060 additions and 534 deletions
26
API/requests/CMakeLists.txt
Normal file
26
API/requests/CMakeLists.txt
Normal 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
10
API/requests/addasset.cpp
Normal 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
19
API/requests/addasset.h
Normal 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);
|
||||
};
|
||||
|
||||
}
|
18
API/requests/listassets.cpp
Normal file
18
API/requests/listassets.cpp
Normal 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
23
API/requests/listassets.h
Normal 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
34
API/requests/login.cpp
Normal 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
23
API/requests/login.h
Normal 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
46
API/requests/poll.cpp
Normal 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
25
API/requests/poll.h
Normal 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
17
API/requests/post.cpp
Normal 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
25
API/requests/post.h
Normal 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
29
API/requests/register.cpp
Normal 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
21
API/requests/register.h
Normal 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
134
API/requests/request.cpp
Normal 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
69
API/requests/request.h
Normal 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
27
API/requests/test.cpp
Normal 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
23
API/requests/test.h
Normal 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;
|
||||
};
|
||||
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue