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

@ -17,4 +17,4 @@ set(SOURCES
target_sources(magpie PRIVATE ${SOURCES})
add_subdirectory(models)
add_subdirectory(requests)

View file

@ -1,5 +1,5 @@
// SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
// SPDX-License-Identifier: GPL-3.0-or-later
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "api.h"
#include <QDebug>
@ -7,466 +7,128 @@
#include <QJsonObject>
#include <QUrlQuery>
#include "codes.h"
#include "finalaction.h"
#include "helpers.h"
#include "requests/test.h"
#include "requests/register.h"
#include "requests/login.h"
#include "requests/poll.h"
#include "requests/listassets.h"
#include "requests/addasset.h"
constexpr const char* json = "application/json";
constexpr const char* urlEncoded = "application/x-www-form-urlencoded";
const std::map<QString, QMetaType::Type> testStructure({
{"type", QMetaType::QString},
{"version", QMetaType::QString},
});
const std::map<QString, QMetaType::Type> resultStructure({
{"result", QMetaType::LongLong},
});
const std::map<QString, QMetaType::Type> tokensStructure({
{"accessToken", QMetaType::QString},
{"renewToken", QMetaType::QString}
});
struct NetworkReplyDeleter {
void operator () (QNetworkReply* reply) {
reply->deleteLater();
}
};
API::API(const QUrl& address, QObject* parent):
API::API (Models::Magpie& magpie, QObject* parent):
QObject(parent),
address(address),
idCounter(0),
magpie(magpie),
network(),
state(NoServer),
accessToken(),
renewToken(),
firstPoll(),
pollReply(),
assets()
{
firstPoll.setSingleShot(true);
firstPoll.setInterval(2000);
connect(&firstPoll, &QTimer::timeout, this, &API::onFirstPollSuccess);
connect(&assets, &Models::Assets::requestAssets, this, &API::requestAssets);
requests()
{}
void API::cancelRequest (RequestId id) {
requests.erase(id);
}
QUrl API::getAddress() const {
return address;
void API::onRequestDone (RequestId id) {
requests.erase(id);
}
API::State API::getState() const {
return state;
}
void API::setTokens(const QString access, const QString &renew) {
accessToken = access;
renewToken = renew;
setState(Authenticating);
startPolling();
}
void API::startPolling() {
qDebug() << "Starting polling...";
if (state != Authenticating)
throw std::runtime_error("Can not start polling in this state: " + std::to_string(state));
sendPoll();
firstPoll.start();
}
void API::setAddress(const QUrl& path) {
if (address == path)
return;
if (state == Authenticated) {
//do something
}
address = path;
emit addressChanged(address);
setState(address.isEmpty() ? NoServer : NotAuthenticated);
}
std::optional<QVariantMap> API::readResult(QNetworkReply* reply) {
QVariant contentType = reply->header(QNetworkRequest::ContentTypeHeader);
if (!
contentType.isValid() ||
contentType.userType() != QMetaType::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();
}
bool API::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;
}
void API::setState(State newState) {
if (newState == state)
return;
state = newState;
emit stateChanged(state);
}
QUrl API::createUrl(const QString &path) const {
QString startingPath = address.path();
QUrl url = address;
url.setPath(startingPath + path);
return url;
}
void API::test(const QString& path, const QJSValue& finished) {
API::RequestId API::test (const QString& path, const QJSValue& finished) {
qDebug() << "Testing" << path;
if (state == Offline)
return callCallback(finished, "Need to be online to test");
if (magpie.getState() == Models::Magpie::Offline)
callCallback(finished, "Need to be online to test"), 0;
QUrl address(path);
QNetworkRequest request(path + "/info");
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
QNetworkReply* reply = network.get(request);
connect(reply, &QNetworkReply::finished,
std::bind(&API::onTestFinished, this, reply, address, finished)
);
}
void API::onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished) {
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
if (error != QNetworkReply::NoError)
return callCallback(finished, reply->errorString());
std::optional<QVariantMap> data = readResult(reply);
if (!validateResponse(data, testStructure))
return callCallback(finished, "Malformed response");
QString type = data->value("type").toString();
if (type != "pica")
return callCallback(finished, "server of this type (" + type + ") is not supported");
QString version = data->value("version").toString();
if (version != "0.0.1")
return callCallback(finished, "server of this version (" + version + ") is not supported");
callCallback(finished, QString(), {QJSValue(true)});
address = ""; //to provoke singal change even if it's the same server
setAddress(addr);
}
void API::sendRegister(const QString& login, const QString& password, const QJSValue& finished) {
qDebug() << "Registering...";
if (state != NotAuthenticated)
return callCallback(finished, "Can not register in current state");
QUrlQuery params({
{"login", login},
{"password", password}
});
QNetworkRequest request(createUrl("/register"));
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished,
std::bind(&API::onRegisterFinished, this, reply, finished)
);
}
void API::onRegisterFinished(QNetworkReply* reply, const QJSValue& finished) const {
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
std::optional<QVariantMap> data = readResult(reply);
std::optional<Codes::Register> code;
QString detail;
bool success = false;
if (validateResponse(data, resultStructure)) {
code = Codes::convertRegister(data->value("result").toInt());
success = code.value() == Codes::Register::success;
if (!success)
detail = Codes::description(code.value());
}
if (error != QNetworkReply::NoError)
return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail));
if (!code)
return callCallback(finished, "Malformed result");
callCallback(finished, detail, {QJSValue(success)});
}
void API::sendLogin(const QString& login, const QString& password, const QJSValue& finished) {
qDebug() << "Logging in...";
if (state != NotAuthenticated)
return callCallback(finished, "Can not register in current state");
QUrlQuery params({
{"login", login},
{"password", password}
});
QNetworkRequest request(createUrl("/login"));
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
setState(Authenticating);
QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished,
std::bind(&API::onLoginFinished, this, reply, finished)
);
}
void API::onLoginFinished(QNetworkReply* reply, const QJSValue& finished) {
State state = NotAuthenticated;
FinalAction action([this, &state]() { //this should be executed on leaving the function in any way
setState(state); //setting the state as a result of this object destruction
}); //this way any error will result in NotAuthenticated, and on success it should be Authenticated
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
std::optional<QVariantMap> data = readResult(reply);
std::optional<Codes::Login> code;
QString detail;
bool success = false;
if (validateResponse(data, resultStructure)) {
code = Codes::convertLogin(data->value("result").toInt());
success = code.value() == Codes::Login::success;
if (!success)
detail = Codes::description(code.value());
}
if (error != QNetworkReply::NoError)
return callCallback(finished, reply->errorString() + (success ? "" : ": " + detail));
if (!code)
return callCallback(finished, "Malformed result");
if (!validateResponse(data, tokensStructure))
return callCallback(finished, "Malformed result: missing tokens");
callCallback(finished, detail, {QJSValue(success)});
state = Authenticating;
accessToken = data->value("accessToken").toString();
renewToken = data->value("renewToken").toString();
emit storeTokens(accessToken, renewToken);
startPolling();
}
void API::addAsset(const QString& title, const QString& icon, const QJSValue &finished) {
qDebug() << "Adding asset...";
if (state != Authenticated)
return callCallback(finished, "Can not add assets in current state");
QUrlQuery params({
{"title", title},
{"icon", icon},
{"currency", "1"}
});
QNetworkRequest request(createUrl("/addAsset"));
request.setHeader(QNetworkRequest::ContentTypeHeader, urlEncoded);
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8());
QNetworkReply* reply = network.post(request, params.toString(QUrl::FullyEncoded).toUtf8());
connect(reply, &QNetworkReply::finished,
this, std::bind(&API::onAssetAdded, this, reply, finished),
Qt::QueuedConnection
);
}
void API::onAssetAdded(QNetworkReply *reply, const QJSValue &finished) {
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
qDebug() << error;
if (error != QNetworkReply::NoError)
return callCallback(finished, reply->errorString());
callCallback(finished, QString(), {QJSValue(true)});
}
void API::sendPoll(bool clear) {
if (accessToken.isEmpty())
throw std::runtime_error("Can not start polling: access token is empty");
QUrl url = createUrl("/poll");
if (clear)
url.setQuery(QUrlQuery({{"clearCache", "all"}}));
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8());
request.setTransferTimeout(30000);
pollReply = std::unique_ptr<QNetworkReply>(network.get(request));
std::unique_ptr<Request::Test> test = std::make_unique<Request::Test>(path);
connect(
pollReply.get(), &QNetworkReply::finished,
this, &API::onPollFinished, Qt::QueuedConnection
test.get(),
&Request::Test::success,
this,
[this, &path, &finished] (const QVariantMap& data) {
callCallback(finished, QString(), {QJSValue(true)});
magpie.forceAddress(path);
}
);
connect(test.get(), &Request::Test::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList(false)));
return registerAndSend(std::move(test));
}
void API::onPollFinished() {
firstPoll.stop();
QNetworkReply::NetworkError error = pollReply->error();
std::optional<QVariantMap> data = readResult(pollReply.get());
Codes::Poll code = Codes::Poll::unknownError;
if (validateResponse(data, resultStructure))
code = Codes::convertPoll(data->value("result").toInt());
else
qDebug() << "";
API::RequestId API::sendRegister (const QString& login, const QString& password, const QJSValue& finished) {
qDebug() << "Registering...";
if (magpie.getState() != Models::Magpie::NotAuthenticated)
return callCallback(finished, "Can not register in current state"), 0;
QString detail = Codes::description(code);
std::unique_ptr<Request::Register> reg = std::make_unique<Request::Register>(login, password, magpie.getAddress());
connect(reg.get(), &Request::Register::success, std::bind(&API::callCallback, this, finished, QString(), QJSValueList{QJSValue(true)}));
connect(reg.get(), &Request::Register::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList{QJSValue(false)}));
return registerAndSend(std::move(reg));
}
if (error != QNetworkReply::NoError)
qDebug() << pollReply->errorString() + ": " + detail;
else
qDebug() << "Poll finished: " + detail;
API::RequestId API::sendLogin (const QString& login, const QString& password, const QJSValue& finished) {
qDebug() << "Logging in...";
if (magpie.getState() != Models::Magpie::NotAuthenticated)
return callCallback(finished, "Can not register in current state"), 0;
bool clear = false;
switch (code) {
case Codes::Poll::success: {
QVariantMap::ConstIterator itr = data->constFind("data");
if (itr == data->constEnd())
qDebug("received a poll that claimed to have some updates, but the \"data\" field is abscent");
const QVariant& vdata = itr.value();
if (vdata.canConvert<QVariantMap>())
clear = handleChanges(qast<QVariantMap>(itr.value()));
else
qDebug("received a poll that claimed to have some updates, but the \"data\" field is not an object");
std::unique_ptr<Request::Login> log = std::make_unique<Request::Login>(login, password, magpie.getAddress());
connect(
log.get(),
&Request::Login::success,
this,
[this, &finished] (const QVariantMap& data) {
callCallback(finished, QString(), {QJSValue(true)});
magpie.setTokens(data.value("accessToken").toString(), data.value("renewToken").toString());
}
//todo handle the result
case Codes::Poll::timeout:
setState(Authenticated);
return sendPoll(clear);
case Codes::Poll::tokenProblem:
case Codes::Poll::replace:
case Codes::Poll::unknownError: //todo this one doesn't actually mean that we can't work for now, the network may be temporarily down or something
pollReply.reset();
return setState(NotAuthenticated);
}
}
bool API::handleChanges(const QVariantMap& changes) {
QVariantMap::ConstIterator itr = changes.constFind("system");
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) {
const QVariantMap& sys = qast<QVariantMap>(itr.value());
QVariantMap::ConstIterator invItr = sys.constFind("invalidate");
if (invItr != sys.constEnd()) {
const QVariant& vinv = invItr.value();
if (vinv.canConvert<bool>() && vinv.toBool())
resetAllModels();
);
connect(
log.get(),
&Request::Login::error,
this,
[this, &finished] (const QString& error) {
callCallback(finished, error, {QJSValue(false)});
magpie.setState(Models::Magpie::NotAuthenticated);
}
}
);
itr = changes.constFind("assets");
if (itr != changes.constEnd() && itr.value().canConvert<QVariantMap>()) {
const QVariantMap& assets = qast<QVariantMap>(itr.value());
QVariantMap::ConstIterator aItr = assets.constFind("invalidate");
if (aItr != assets.constEnd()) {
const QVariant& vinv = aItr.value();
if (vinv.canConvert<bool>() && vinv.toBool())
API::assets.clear();
}
aItr = assets.constFind("added");
if (aItr != assets.constEnd() && aItr.value().canConvert<QVariantList>()) {
std::deque<Models::Asset> added;
if (!Models::Assets::deserialize(qast<QVariantList>(aItr.value()), added))
qDebug() << "Error deserializng added assets";
else
API::assets.addAssets(added);
}
}
return true;
magpie.setState(Models::Magpie::Authenticating);
return registerAndSend(std::move(log));
}
void API::resetAllModels() {
assets.clear();
API::RequestId API::addAsset (const QString& title, const QString& icon, const QJSValue& finished) {
qDebug() << "Adding asset...";
if (magpie.getState() != Models::Magpie::Authenticated)
return callCallback(finished, "Can not add assets in current state"), 0;
auto add = std::make_unique<Request::AddAsset>(title, icon, QColor::fromString("black"), 1, magpie.getAddress());
add->setAuthorizationToken(magpie.getAccessToken());
connect(add.get(), &Request::AddAsset::success, std::bind(&API::callCallback, this, finished, QString(), QJSValueList{QJSValue(true)}));
connect(add.get(), &Request::AddAsset::error, std::bind(&API::callCallback, this, finished, std::placeholders::_1, QJSValueList{QJSValue(false)}));
return registerAndSend(std::move(add));
}
void API::onFirstPollSuccess() {
setState(Authenticated);
}
void API::requestAssets() {
if (state != Authenticated) {
qDebug() << "An attempt to request assets on unauthenticated state";
assets.receivedAssets({});
return;
}
API::RequestId API::requestAssets (const SuccessListHandler& success, const ErrorHandler& error) {
qDebug() << "Requesting assets...";
QUrl url = createUrl("/listAssets");
QNetworkRequest request(url);
request.setHeader(QNetworkRequest::ContentTypeHeader, json);
request.setRawHeader("Authorization", "Bearer " + accessToken.toUtf8());
QNetworkReply* reply = network.get(request);
connect(
reply, &QNetworkReply::finished,
this, std::bind(&API::responseAssets, this, reply)
);
auto list = std::make_unique<Request::ListAssets>(magpie.getAddress());
list->setAuthorizationToken(magpie.getAccessToken());
connect(list.get(), &Request::ListAssets::success, success);
connect(list.get(), &Request::ListAssets::error, error);
return registerAndSend(std::move(list));
}
void API::responseAssets(QNetworkReply *reply) {
std::deque<Models::Asset> result;
FinalAction action([this, &result]() {
assets.receivedAssets(result);
});
std::unique_ptr<QNetworkReply, NetworkReplyDeleter> rpl(reply);
QNetworkReply::NetworkError error = reply->error();
if (error != QNetworkReply::NoError) {
qDebug() << "Error receiving assets:" << reply->errorString();
return;
}
std::optional<QVariantMap> data = readResult(reply);
if (!data) {
qDebug() << "Error receiving assets: bad data";
return;
}
QVariantMap::ConstIterator itr = data->find("assets");
if (itr == data->constEnd() || !itr->canConvert<QVariantList>()) {
qDebug() << "Error receiving assets: assets are missing or not in an array";
return;
}
if (!Models::Assets::deserialize(qast<QVariantList>(itr.value()), result)) {
qDebug() << "Error deserializng assets";
result.clear();
}
qDebug() << "Assets successfully received";
//final action goes automatically
API::RequestId API::poll (const SuccessMapHandler& success, const ErrorHandler& error, bool clear) {
auto poll = std::make_unique<Request::Poll>(magpie.getAddress(), clear);
poll->setAuthorizationToken(magpie.getAccessToken());
connect(poll.get(), &Request::Poll::success, success);
connect(poll.get(), &Request::Poll::error, error);
return registerAndSend(std::move(poll));
}
void API::callCallback(const QJSValue& callback, const QString& error, const QJSValueList& arguments) const {
API::RequestId API::registerAndSend (std::unique_ptr<Request::Request> request) {
Request::Request* req = request.get();
requests.emplace(++idCounter, std::move(request));
connect(req, &Request::Request::done, std::bind(&API::onRequestDone, this, idCounter));
req->send(network);
return idCounter;
}
void API::callCallback (const QJSValue& callback, const QString& error, const QJSValueList& arguments) const {
if (callback.isCallable()) {
if (error.isEmpty())
callback.call(QJSValueList({QJSValue(QJSValue::NullValue)}) + arguments);
@ -474,4 +136,3 @@ void API::callCallback(const QJSValue& callback, const QString& error, const QJS
callback.call({QJSValue(error)});
}
}

View file

@ -6,6 +6,7 @@
#include <memory>
#include <optional>
#include <map>
#include <functional>
#include <QObject>
#include <QString>
@ -13,80 +14,45 @@
#include <QJSValue>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QTimer>
#include "models/assets.h"
#include "models/magpie.h"
#include "requests/request.h"
class Root;
class API : public QObject {
friend class Root;
Q_OBJECT
public:
enum State {
Offline,
NoServer,
NotAuthenticated,
Authenticating,
Authenticated
};
Q_ENUM(State)
private:
Q_PROPERTY(QUrl address READ getAddress NOTIFY addressChanged)
Q_PROPERTY(State state READ getState NOTIFY stateChanged)
public:
explicit API(const QUrl& path = QString(), QObject* parent = nullptr);
using SuccessMapHandler = std::function<void(const QVariantMap&)>;
using SuccessListHandler = std::function<void(const QVariantList&)>;
using ErrorHandler = std::function<void(const QString&, const std::optional<QVariantMap>&)>;
using RequestId = unsigned int;
QUrl getAddress() const;
State getState() const;
void setTokens(const QString access, const QString& renew);
void startPolling();
explicit API(Models::Magpie& magpie, QObject* parent = nullptr);
signals:
void addressChanged(const QUrl& path);
void stateChanged(State state);
void storeTokens(const QString& access, const QString& renew);
RequestId requestAssets(const SuccessListHandler& success, const ErrorHandler& error);
RequestId poll(const SuccessMapHandler& success, const ErrorHandler& error, bool clear = false);
static const RequestId none = 0;
public slots:
void test(const QString& path, const QJSValue& finished = QJSValue());
void sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
void sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
void addAsset(const QString& title, const QString& icon, const QJSValue& finished = QJSValue());
void cancelRequest(RequestId id);
RequestId test(const QString& path, const QJSValue& finished = QJSValue());
RequestId sendRegister(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
RequestId sendLogin(const QString& login, const QString& password, const QJSValue& finished = QJSValue());
RequestId addAsset(const QString& title, const QString& icon, const QJSValue& finished = QJSValue());
private slots:
void onTestFinished(QNetworkReply* reply, const QUrl& addr, const QJSValue& finished);
void onRegisterFinished(QNetworkReply* reply, const QJSValue& finished) const;
void onLoginFinished(QNetworkReply* reply, const QJSValue& finished);
void onAssetAdded(QNetworkReply* reply, const QJSValue& finished);
void onPollFinished();
void onFirstPollSuccess();
void requestAssets();
void responseAssets(QNetworkReply* reply);
void onRequestDone(RequestId id);
private:
void callCallback(const QJSValue& callback, const QString& error = QString(), const QJSValueList& arguments = QJSValueList()) const;
void setAddress(const QUrl& path);
static std::optional<QVariantMap> readResult(QNetworkReply* reply);
static bool validateResponse(const std::optional<QVariantMap>& data, const std::map<QString, QMetaType::Type>& structure);
void setState(State newState);
QUrl createUrl(const QString& path) const;
void sendPoll(bool clear = false);
bool handleChanges(const QVariantMap& changes);
void resetAllModels();
RequestId registerAndSend(std::unique_ptr<Request::Request> request);
private:
QUrl address;
RequestId idCounter;
Models::Magpie& magpie;
QNetworkAccessManager network;
State state;
QString accessToken;
QString renewToken;
QTimer firstPoll;
std::map<RequestId, std::unique_ptr<Request::Request>> requests;
std::unique_ptr<QNetworkReply> pollReply;
public:
Models::Assets assets;
};

View file

@ -6,6 +6,8 @@
#include <QVariant>
#include <stdexcept>
#define UNUSED(X) (void)(X)
template <class T>
const T& qast(const QVariant& variant) {
if (variant.userType() == qMetaTypeId<T>())

View file

@ -1,12 +0,0 @@
# SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
# SPDX-License-Identifier: GPL-3.0-or-later
set(HEADERS
assets.h
)
set(SOURCES
assets.cpp
)
target_sources(magpie PRIVATE ${SOURCES})

View file

@ -1,139 +0,0 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#include "assets.h"
#include "../helpers.h"
Models::Assets::Assets (QObject* parent):
QAbstractListModel(parent),
records(),
state(State::initial)
{}
void Models::Assets::clear () {
beginResetModel();
records.clear();
endResetModel();
}
void Models::Assets::addAsset (const Asset& asset) {
QModelIndex index = getIndex(asset.id);
if (index.isValid())
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model");
beginInsertRows(QModelIndex(), records.size(), records.size() + 1);
records.push_back(asset);
endInsertRows();
}
void Models::Assets::addAssets (const std::deque<Asset>& assets) {
if (assets.empty())
return;
for (const Asset& asset : assets)
if (getIndex(asset.id).isValid())
throw std::runtime_error("An attempt to insert a duplicating Asset to an asset model (bulk)");
beginInsertRows(QModelIndex(), records.size(), records.size() + assets.size());
for (const Asset& asset : assets)
records.push_back(asset);
endInsertRows();
}
void Models::Assets::deleteAsset (unsigned int id) {
QModelIndex index = getIndex(id);
if (!index.isValid())
throw std::runtime_error("An attempt to insert to delete non existing Asset from asset model");
int row = index.row();
beginRemoveRows(QModelIndex(), row, row + 1);
records.erase(records.begin() + row);
if (state == State::syncronized) //give a second thought
state = State::initial;
endRemoveRows();
}
int Models::Assets::rowCount (const QModelIndex& parent) const {
//For list models only the root node (an invalid parent) should return the
//list's size. For all other (valid) parents, rowCount() should return 0 so
//that it does not become a tree model.
if (parent.isValid())
return 0;
return records.size();
}
QHash<int, QByteArray> Models::Assets::roleNames () const {
static const QHash<int, QByteArray> roleNames{
{Title, "title"}, {Icon, "icon"}, {Balance, "balance"}, {Archived, "archived"}
};
return roleNames;
}
bool Models::Assets::canFetchMore (const QModelIndex& parent) const {
return state == State::initial;
}
void Models::Assets::fetchMore (const QModelIndex& parent) {
if (state != State::initial)
return;
state = State::requesting;
emit requestAssets();
}
QVariant Models::Assets::data (const QModelIndex& index, int role) const {
if (!index.isValid())
return QVariant();
int row = index.row();
if (row >= 0 && row < records.size()) {
switch (role) {
case Qt::DisplayRole:
case Title:
return records[row].title;
case Icon:
return records[row].icon;
case Balance:
return records[row].balance;
case Archived:
return records[row].archived;
}
}
return QVariant();
}
bool Models::Assets::deserialize (const QVariantList& from, std::deque<Asset>& out) {
for (const QVariant& item : from) {
if (!item.canConvert<QVariantMap>())
return false;
const QVariantMap& ser = qast<QVariantMap>(item);
Asset& asset = out.emplace_back();
asset.title = ser.value("title").toString();
asset.icon = ser.value("icon").toString();
asset.archived = ser.value("archived").toBool();
asset.id = ser.value("id").toUInt();
}
return true;
}
void Models::Assets::receivedAssets (const std::deque<Asset>& assets) {
beginResetModel();
records = assets;
state = State::syncronized;
endResetModel();
}
QModelIndex Models::Assets::getIndex (unsigned int id) const {
for (std::size_t i = 0; i < records.size(); ++i) {
if (records[i].id == id)
return createIndex(i, 0, &records[i]);
}
return QModelIndex();
}

View file

@ -1,72 +0,0 @@
//SPDX-FileCopyrightText: 2023 Yury Gubich <blue@macaw.me>
//SPDX-License-Identifier: GPL-3.0-or-later
#pragma once
#include <deque>
#include <QString>
#include <QAbstractListModel>
#include <qqmlregistration.h>
namespace Models {
struct Asset {
unsigned int id;
QString title;
QString icon;
double balance;
bool archived;
unsigned int currency;
};
class Assets : public QAbstractListModel {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
public:
explicit Assets (QObject* parent = nullptr);
enum Roles {
Title = Qt::UserRole + 1,
Icon,
Balance,
Archived
};
void clear();
void addAsset(const Asset& asset);
void addAssets(const std::deque<Asset>& assets);
void deleteAsset(unsigned int id);
//Basic functionality:
int rowCount (const QModelIndex& parent = QModelIndex()) const override;
QHash<int, QByteArray> roleNames () const override;
//Fetch data dynamically:
bool canFetchMore (const QModelIndex& parent) const override;
void fetchMore (const QModelIndex& parent) override;
QVariant data (const QModelIndex& index, int role = Qt::DisplayRole) const override;
static bool deserialize(const QVariantList& from, std::deque<Asset>& out);
signals:
void requestAssets();
public slots:
void receivedAssets(const std::deque<Asset>& assets);
private:
QModelIndex getIndex(unsigned int id) const;
private:
enum class State {
initial,
requesting,
syncronized
};
State state;
std::deque<Asset> records;
};
}

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;
};
}