diff options
author | Juha Vuolle <juha.vuolle@qt.io> | 2023-06-12 11:23:19 +0300 |
---|---|---|
committer | Juha Vuolle <juha.vuolle@qt.io> | 2023-12-08 15:53:33 +0200 |
commit | e560adef213301318dcc13d4db155624846e0420 (patch) | |
tree | 237ffa17c837ee0f270885641b781d9bb47c6fd6 /src/network/access | |
parent | f587ba1036164691a0981897397bdcc8f3472438 (diff) |
Add REST client convenience wrappers
[ChangeLog][QtNetwork][QRestAccessManager] Added new convenience
classes QRestAccessManager and QRestReply for typical RESTful
client application usage
Task-number: QTBUG-114637
Task-number: QTBUG-114701
Change-Id: I65057e56bf27f365b54bfd528565efd5f09386aa
Reviewed-by: MÃ¥rten Nordheim <marten.nordheim@qt.io>
Reviewed-by: Mate Barany <mate.barany@qt.io>
Reviewed-by: Marc Mutz <marc.mutz@qt.io>
Diffstat (limited to 'src/network/access')
-rw-r--r-- | src/network/access/qrestaccessmanager.cpp | 819 | ||||
-rw-r--r-- | src/network/access/qrestaccessmanager.h | 110 | ||||
-rw-r--r-- | src/network/access/qrestaccessmanager_p.h | 87 | ||||
-rw-r--r-- | src/network/access/qrestreply.cpp | 364 | ||||
-rw-r--r-- | src/network/access/qrestreply.h | 55 | ||||
-rw-r--r-- | src/network/access/qrestreply_p.h | 39 |
6 files changed, 1474 insertions, 0 deletions
diff --git a/src/network/access/qrestaccessmanager.cpp b/src/network/access/qrestaccessmanager.cpp new file mode 100644 index 0000000000..c4efa846a2 --- /dev/null +++ b/src/network/access/qrestaccessmanager.cpp @@ -0,0 +1,819 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qrestaccessmanager.h" +#include "qrestaccessmanager_p.h" +#include "qrestreply.h" + +#include <QtNetwork/qhttpmultipart.h> +#include <QtNetwork/qnetworkaccessmanager.h> +#include <QtNetwork/qnetworkreply.h> + +#if QT_CONFIG(ssl) +#include <QtNetwork/qsslsocket.h> +#endif + +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qthread.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +Q_LOGGING_CATEGORY(lcQrest, "qt.network.access.rest") + +/*! + + \class QRestAccessManager + \brief The QRestAccessManager is a networking convenience class for RESTful + client applications. + \since 6.7 + + \ingroup network + \inmodule QtNetwork + \reentrant + + QRestAccessManager provides a networking API for typical REST client + applications. It provides the means to issue HTTP requests such as GET + and POST. The responses to these requests can be handled with traditional + Qt signal and slot mechanisms, as well as by providing callbacks + directly - see \l {Issuing Network Requests and Handling Replies}. + + The class is a wrapper on top of QNetworkAccessManager, and it both amends + convenience methods and omits typically less used features. These + features are still accessible by configuring the underlying + QNetworkAccessManager directly. QRestAccessManager is closely related to + the QRestReply class, which it returns when issuing network requests. + + QRestAccessManager and related QRestReply classes can only be used in the + thread they live in. For further information see + \l {QObject#Thread Affinity}{QObject thread affinity} documentation. + + \section1 Issuing Network Requests and Handling Replies + + Network requests are initiated with a function call corresponding to + the desired HTTP method, such as \c get() and \c post(). + + \section2 Using Signals and Slots + + The function returns a QRestReply* object, whose signals can be used + to follow up on the completion of the request in a traditional + Qt-signals-and-slots way. + + Here's an example of how you could send a GET request and handle the + response: + + \snippet code/src_network_access_qrestaccessmanager.cpp 0 + + \section2 Using Callbacks and Context Objects + + The functions also take a context object of QObject (subclass) type + and a callback function as parameters. The callback takes one QRestReply* + as a parameter. The callback can be any callable, incl. a + pointer-to-member-function.. + + These callbacks are invoked when the QRestReply has finished processing + (also in the case the processing finished due to an error). + + The context object can be \c nullptr, although, generally speaking, + this is discouraged. Using a valid context object ensures that if the + context object is destroyed during request processing, the callback will + not be called. Stray callbacks which access a destroyed context is a source + of application misbehavior. + + Here's an example of how you could send a GET request and check the + response: + + \snippet code/src_network_access_qrestaccessmanager.cpp 1 + + Many of the functions take in data for sending to a server. The data is + supplied as the second parameter after the request. + + Here's an example of how you could send a POST request and check the + response: + + \snippet code/src_network_access_qrestaccessmanager.cpp 2 + + \section2 Supported data types + + The following table summarizes the methods and the supported data types. + \c X means support. + + \table + \header + \li Data type + \li \c get() + \li \c post() + \li \c put() + \li \c head() + \li \c deleteResource() + \row + \li No data + \li X + \li - + \li - + \li X + \li X + \row + \li QByteArray + \li X + \li X + \li X + \li - + \li - + \row + \li QJsonObject *) + \li X + \li X + \li X + \li - + \li - + \row + \li QJsonArray *) + \li - + \li X + \li X + \li - + \li - + \row + \li QVariantMap **) + \li - + \li X + \li X + \li - + \li - + \row + \li QHttpMultiPart + \li - + \li X + \li X + \li - + \li - + \row + \li QIODevice + \li X + \li X + \li X + \li - + \li - + \endtable + + *) QJsonObject and QJsonArray are sent in \l QJsonDocument::Compact format, + and the \c Content-Type header is set to \c {application/json} if the + \c Content-Type header was not set + + **) QVariantMap is converted to and treated as a QJsonObject + + \sa QRestReply, QNetworkRequestFactory, QNetworkAccessManager +*/ + +/*! + \fn void QRestAccessManager::authenticationRequired(QRestReply *reply, + QAuthenticator *authenticator) + + This signal is emitted when the final server requires authentication. + The authentication relates to the provided \a reply instance, and any + credentials are to be filled in the provided \a authenticator instance. + + See \l QNetworkAccessManager::authenticationRequired() for details. +*/ + +/*! + \fn void QRestAccessManager::proxyAuthenticationRequired( + const QNetworkProxy &proxy, QAuthenticator *authenticator) + + This signal is emitted when a proxy authentication requires action. + The proxy details are in \a proxy object, and any credentials are + to be filled in the provided \a authenticator object. + + See \l QNetworkAccessManager::proxyAuthenticationRequired() for details. +*/ + +/*! + \fn void QRestAccessManager::requestFinished(QRestReply *reply) + + This signal is emitted whenever a pending network reply is + finished. \a reply parameter will contain a pointer to the + reply that has just finished. This signal is emitted in tandem + with the QRestReply::finished() signal. QRestReply provides + functions for checking the status of the request, as well as for + acquiring any received data. + + \note Do not delete \a reply object in the slot connected to this + signal. Use deleteLater() if needed. See also \l deletesRepliesOnFinished(). + + \sa QRestReply::finished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP GET} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 3 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, const QByteArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP GET} based on \a request and provided \a data. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 4 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, const QJsonObject &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::get( + const QNetworkRequest &request, QIODevice *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QJsonObject &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP POST} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 5 + + Alternatively, the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + The \c post() method always requires \a data parameter. The following + data types are supported: + \list + \li QByteArray + \li QJsonObject *) + \li QJsonArray *) + \li QVariantMap **) + \li QHttpMultiPart* + \li QIODevice* + \endlist + + *) Sent in \l QJsonDocument::Compact format, and the + \c Content-Type header is set to \c {application/json} if the + \c Content-Type header was not set + **) QVariantMap is converted to and treated as a QJsonObject + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QJsonArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QVariantMap &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, const QByteArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, QHttpMultiPart *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::post( + const QNetworkRequest &request, QIODevice *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QJsonObject &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP PUT} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 6 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + The \c put() method always requires \a data parameter. The following + data types are supported: + \list + \li QByteArray + \li QJsonObject *) + \li QJsonArray *) + \li QVariantMap **) + \li QHttpMultiPart* + \li QIODevice* + \endlist + + *) Sent in \l QJsonDocument::Compact format, and the + \c Content-Type header is set to \c {application/json} if the + \c Content-Type header was not set + **) QVariantMap is converted to and treated as a QJsonObject + + \sa QRestReply, QRestReply::finished(), QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QJsonArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QVariantMap &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, const QByteArray &data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, QHttpMultiPart *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::put( + const QNetworkRequest &request, QIODevice *data, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + \overload +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::head( + const QNetworkRequest &request, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP HEAD} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 7 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \c head() request does not support providing data. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/*! + \fn template<typename Functor, if_compatible_callback<Functor>> QRestReply *QRestAccessManager::deleteResource( + const QNetworkRequest &request, + const ContextTypeForFunctor<Functor> *context, + Functor &&callback) + + Issues an \c {HTTP DELETE} based on \a request. + + The optional \a callback and \a context object can be provided for + handling the request completion as illustrated below: + + \snippet code/src_network_access_qrestaccessmanager.cpp 8 + + Alternatively the signals of the returned QRestReply* object can be + used. For further information see + \l {Issuing Network Requests and Handling Replies}. + + \c deleteResource() request does not support providing data. + + \sa QRestReply, QRestReply::finished(), + QRestAccessManager::requestFinished() +*/ + +/* + Memory management/object ownership: + - QRestAM is parent of QNAM and QRestReplies + - QRestReplies are parents of QNetworkReplies +*/ + +/*! + Constructs a QRestAccessManager and sets \a parent as the parent object. +*/ +QRestAccessManager::QRestAccessManager(QObject *parent) + : QObject(*new QRestAccessManagerPrivate, parent) +{ + Q_D(QRestAccessManager); + d->ensureNetworkAccessManager(); +} + +/*! + Destroys the QRestAccessManager object and frees up any + resources, including any unfinished QRestReply objects. +*/ +QRestAccessManager::~QRestAccessManager() + = default; + +/*! + Returns whether QRestAccessManager is currently configured to automatically + delete replies once they have finished. By default this is \c true. + + \sa setDeletesRepliesOnFinished() +*/ +bool QRestAccessManager::deletesRepliesOnFinished() const +{ + Q_D(const QRestAccessManager); + return d->deletesRepliesOnFinished; +} + +/*! + Enables or disables automatic deletion of QRestReply instances + once the request has finished, according to the provided + \a autoDelete parameter. The deletion is done with deleteLater() + so that using the replies in directly-connected slots or callbacks is safe. + + \sa deletesRepliesOnFinished() +*/ +void QRestAccessManager::setDeletesRepliesOnFinished(bool autoDelete) +{ + Q_D(QRestAccessManager); + d->deletesRepliesOnFinished = autoDelete; +} + +/*! + Aborts all unfinished network requests. Calling this function is same + as calling QRestReply::abort() for all individual unfinished requests. + + \sa QRestReply::abort(), QNetworkReply::abort() +*/ +void QRestAccessManager::abortRequests() +{ + Q_D(QRestAccessManager); + + // Make copy of the reply container, as it might get modified when + // aborting individual requests if they finish immediately + const auto requests = d->activeRequests; + for (const auto &[req, _] : requests.asKeyValueRange()) + req->abort(); +} + +/*! + Returns the underlying QNetworkAccessManager instance. The instance + can be used for accessing less-frequently used features and configurations. + + \sa QNetworkAccessManager +*/ +QNetworkAccessManager *QRestAccessManager::networkAccessManager() const +{ + Q_D(const QRestAccessManager); + return d->qnam; +} + +QRestAccessManagerPrivate::QRestAccessManagerPrivate() + = default; + +QRestAccessManagerPrivate::~QRestAccessManagerPrivate() +{ + if (!activeRequests.isEmpty()) { + qCWarning(lcQrest, "Access manager destroyed while %lld requests were still in progress", + qlonglong(activeRequests.size())); + } +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QJsonObject &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QJsonArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->post(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QVariantMap &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + return postWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + const QByteArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + QHttpMultiPart *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::postWithDataImpl(const QNetworkRequest &request, + QIODevice *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->post(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::getNoDataImpl(const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->get(request); }, context, slot); +} + +QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request, + const QByteArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request, + const QJsonObject &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->get(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::getWithDataImpl(const QNetworkRequest &request, + QIODevice *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->get(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::deleteResourceNoDataImpl(const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->deleteResource(request); }, context, slot); +} + +QRestReply *QRestAccessManager::headNoDataImpl(const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->head(request); }, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QJsonObject &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QJsonArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&](auto req, auto json) { return d->qnam->put(req, json); }, + data, request, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QVariantMap &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + return putWithDataImpl(request, QJsonObject::fromVariantMap(data), context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + const QByteArray &data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, + QHttpMultiPart *data, const QObject *context, + QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot); +} + +QRestReply *QRestAccessManager::putWithDataImpl(const QNetworkRequest &request, QIODevice *data, + const QObject *context, QtPrivate::QSlotObjectBase *slot) +{ + Q_D(QRestAccessManager); + return d->executeRequest([&]() { return d->qnam->put(request, data); }, context, slot); +} + +QRestReply *QRestAccessManagerPrivate::createActiveRequest(QNetworkReply *networkReply, + const QObject *contextObject, + QtPrivate::QSlotObjectBase *slot) +{ + Q_Q(QRestAccessManager); + Q_ASSERT(networkReply); + auto restReply = new QRestReply(networkReply, q); + QtPrivate::SlotObjSharedPtr slotPtr(QtPrivate::SlotObjUniquePtr{slot}); // adopts + activeRequests.insert(restReply, CallerInfo{contextObject, slotPtr}); + + // If context object is provided, use it with connect => context object lifecycle is considered + const QObject *context = contextObject ? contextObject : q; + QObject::connect(networkReply, &QNetworkReply::finished, context, [restReply, this]() { + handleReplyFinished(restReply); + }); + // Safe guard in case reply is destroyed before it's finished + QObject::connect(restReply, &QRestReply::destroyed, q, [restReply, this]() { + activeRequests.remove(restReply); + }); + // If context object is destroyed, clean up any possible replies it had associated with it + if (contextObject) { + QObject::connect(contextObject, &QObject::destroyed, q, [restReply, this]() { + activeRequests.remove(restReply); + }); + } + return restReply; +} + +void QRestAccessManagerPrivate::verifyThreadAffinity(const QObject *contextObject) +{ + Q_Q(QRestAccessManager); + if (QThread::currentThread() != q->thread()) { + qCWarning(lcQrest, "QRestAccessManager can only be called in the thread it belongs to"); + Q_ASSERT(false); + } + if (contextObject && (contextObject->thread() != q->thread())) { + qCWarning(lcQrest, "QRestAccessManager: the context object must reside in the same thread"); + Q_ASSERT(false); + } +} + +void QRestAccessManagerPrivate::ensureNetworkAccessManager() +{ + Q_Q(QRestAccessManager); + if (!qnam) { + qnam = new QNetworkAccessManager(q); + connect(qnam, &QNetworkAccessManager::authenticationRequired, this, + &QRestAccessManagerPrivate::handleAuthenticationRequired); +#ifndef QT_NO_NETWORKPROXY + QObject::connect(qnam, &QNetworkAccessManager::proxyAuthenticationRequired, + q, &QRestAccessManager::proxyAuthenticationRequired); +#endif + } +} + +void QRestAccessManagerPrivate::handleReplyFinished(QRestReply *restReply) +{ + Q_Q(QRestAccessManager); + + auto request = activeRequests.find(restReply); + if (request == activeRequests.end()) { + qCWarning(lcQrest, "Unexpected reply received, ignoring"); + return; + } + + CallerInfo caller = request.value(); + activeRequests.erase(request); + + if (caller.slot) { + // Callback was provided. If we have context object, use it. + // For clarity: being here with a context object means it has not been destroyed + // while the request has been in progress + void *argv[] = { nullptr, &restReply }; + QObject *context = caller.contextObject + ? const_cast<QObject*>(caller.contextObject) : nullptr; + caller.slot->call(context, argv); + } + if (restReply->hasError()) + emit restReply->errorOccurred(restReply); + emit restReply->finished(restReply); + emit q->requestFinished(restReply); + + if (deletesRepliesOnFinished) + restReply->deleteLater(); +} + +void QRestAccessManagerPrivate::handleAuthenticationRequired(QNetworkReply *networkReply, + QAuthenticator *authenticator) +{ + Q_Q(QRestAccessManager); + QRestReply *restReply = restReplyFromNetworkReply(networkReply); + if (restReply) + emit q->authenticationRequired(restReply, authenticator); + else + qCWarning(lcQrest, "No matching QRestReply for authentication, ignoring."); +} + +QRestReply *QRestAccessManagerPrivate::restReplyFromNetworkReply(QNetworkReply *networkReply) +{ + for (const auto &[restReply,_] : activeRequests.asKeyValueRange()) { + if (restReply->networkReply() == networkReply) + return restReply; + } + return nullptr; +} + +QT_END_NAMESPACE + +#include "moc_qrestaccessmanager.cpp" diff --git a/src/network/access/qrestaccessmanager.h b/src/network/access/qrestaccessmanager.h new file mode 100644 index 0000000000..7b67486a2e --- /dev/null +++ b/src/network/access/qrestaccessmanager.h @@ -0,0 +1,110 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTACCESSMANAGER_H +#define QRESTACCESSMANAGER_H + +#include <QtNetwork/qnetworkaccessmanager.h> + +QT_BEGIN_NAMESPACE + +class QRestReply; + +#define QREST_METHOD_WITH_DATA(METHOD, DATA) \ +public: \ +template <typename Functor, if_compatible_callback<Functor> = true> \ +QRestReply *METHOD(const QNetworkRequest &request, DATA data, \ + const ContextTypeForFunctor<Functor> *context, \ + Functor &&callback) \ +{ \ + return METHOD##WithDataImpl(request, data, context, \ + QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \ +} \ +QRestReply *METHOD(const QNetworkRequest &request, DATA data) \ +{ \ + return METHOD##WithDataImpl(request, data, nullptr, nullptr); \ +} \ +private: \ +QRestReply *METHOD##WithDataImpl(const QNetworkRequest &request, DATA data, \ + const QObject *context, QtPrivate::QSlotObjectBase *slot); \ +/* end */ + + +#define QREST_METHOD_NO_DATA(METHOD) \ +public: \ +template <typename Functor, if_compatible_callback<Functor> = true> \ +QRestReply *METHOD(const QNetworkRequest &request, \ + const ContextTypeForFunctor<Functor> *context, \ + Functor &&callback) \ +{ \ + return METHOD##NoDataImpl(request, context, \ + QtPrivate::makeCallableObject<CallbackPrototype>(std::forward<Functor>(callback))); \ +} \ +QRestReply *METHOD(const QNetworkRequest &request) \ +{ \ + return METHOD##NoDataImpl(request, nullptr, nullptr); \ +} \ +private: \ +QRestReply *METHOD##NoDataImpl(const QNetworkRequest &request, \ + const QObject *context, QtPrivate::QSlotObjectBase *slot); \ +/* end */ + +class QRestAccessManagerPrivate; +class Q_NETWORK_EXPORT QRestAccessManager : public QObject +{ + Q_OBJECT + + using CallbackPrototype = void(*)(QRestReply*); + template <typename Functor> + using ContextTypeForFunctor = typename QtPrivate::ContextTypeForFunctor<Functor>::ContextType; + template <typename Functor> + using if_compatible_callback = std::enable_if_t< + QtPrivate::AreFunctionsCompatible<CallbackPrototype, Functor>::value, bool>; +public: + explicit QRestAccessManager(QObject *parent = nullptr); + ~QRestAccessManager() override; + + QNetworkAccessManager *networkAccessManager() const; + + bool deletesRepliesOnFinished() const; + void setDeletesRepliesOnFinished(bool autoDelete); + + void abortRequests(); + + QREST_METHOD_NO_DATA(deleteResource) + QREST_METHOD_NO_DATA(head) + QREST_METHOD_NO_DATA(get) + QREST_METHOD_WITH_DATA(get, const QByteArray &) + QREST_METHOD_WITH_DATA(get, const QJsonObject &) + QREST_METHOD_WITH_DATA(get, QIODevice *) + QREST_METHOD_WITH_DATA(post, const QJsonObject &) + QREST_METHOD_WITH_DATA(post, const QJsonArray &) + QREST_METHOD_WITH_DATA(post, const QVariantMap &) + QREST_METHOD_WITH_DATA(post, const QByteArray &) + QREST_METHOD_WITH_DATA(post, QHttpMultiPart *) + QREST_METHOD_WITH_DATA(post, QIODevice *) + QREST_METHOD_WITH_DATA(put, const QJsonObject &) + QREST_METHOD_WITH_DATA(put, const QJsonArray &) + QREST_METHOD_WITH_DATA(put, const QVariantMap &) + QREST_METHOD_WITH_DATA(put, const QByteArray &) + QREST_METHOD_WITH_DATA(put, QHttpMultiPart *) + QREST_METHOD_WITH_DATA(put, QIODevice *) + +Q_SIGNALS: +#ifndef QT_NO_NETWORKPROXY + void proxyAuthenticationRequired(const QNetworkProxy &proxy, QAuthenticator *authenticator); +#endif + void authenticationRequired(QRestReply *reply, QAuthenticator *authenticator); + void requestFinished(QRestReply *reply); + +private: + Q_DECLARE_PRIVATE(QRestAccessManager) + Q_DISABLE_COPY(QRestAccessManager) +}; + +#undef QREST_METHOD_NO_DATA +#undef QREST_METHOD_WITH_DATA + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/qrestaccessmanager_p.h b/src/network/access/qrestaccessmanager_p.h new file mode 100644 index 0000000000..91c6f89dd0 --- /dev/null +++ b/src/network/access/qrestaccessmanager_p.h @@ -0,0 +1,87 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTACCESSMANAGER_P_H +#define QRESTACCESSMANAGER_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include "qrestaccessmanager.h" +#include "private/qobject_p.h" + +#include <QtNetwork/qnetworkaccessmanager.h> + +#include <QtCore/qjsonarray.h> +#include <QtCore/qhash.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> + +QT_BEGIN_NAMESPACE + +class QRestReply; +class QRestAccessManagerPrivate : public QObjectPrivate +{ +public: + QRestAccessManagerPrivate(); + ~QRestAccessManagerPrivate() override; + + void ensureNetworkAccessManager(); + + QRestReply *createActiveRequest(QNetworkReply *networkReply, const QObject *contextObject, + QtPrivate::QSlotObjectBase *slot); + + void removeActiveRequest(QRestReply *restReply); + void handleReplyFinished(QRestReply *restReply); + void handleAuthenticationRequired(QNetworkReply *networkReply, QAuthenticator *authenticator); + QRestReply *restReplyFromNetworkReply(QNetworkReply *networkReply); + + template<typename Functor> + QRestReply *executeRequest(Functor requestOperation, + const QObject *context, QtPrivate::QSlotObjectBase *slot) + { + verifyThreadAffinity(context); + QNetworkReply *reply = requestOperation(); + return createActiveRequest(reply, context, slot); + } + + template<typename Functor, typename Json> + QRestReply *executeRequest(Functor requestOperation, Json jsonData, + const QNetworkRequest &request, + const QObject *context, QtPrivate::QSlotObjectBase *slot) + { + verifyThreadAffinity(context); + QNetworkRequest req(request); + if (!request.header(QNetworkRequest::ContentTypeHeader).isValid()) { + req.setHeader(QNetworkRequest::ContentTypeHeader, + QLatin1StringView{"application/json"}); + } + QJsonDocument json(jsonData); + QNetworkReply *reply = requestOperation(req, json.toJson(QJsonDocument::Compact)); + return createActiveRequest(reply, context, slot); + } + + void verifyThreadAffinity(const QObject *contextObject); + + struct CallerInfo { + const QObject *contextObject = nullptr; + QtPrivate::SlotObjSharedPtr slot; + }; + QHash<QRestReply*, CallerInfo> activeRequests; + + QNetworkAccessManager *qnam = nullptr; + bool deletesRepliesOnFinished = true; + Q_DECLARE_PUBLIC(QRestAccessManager) +}; + +QT_END_NAMESPACE + +#endif diff --git a/src/network/access/qrestreply.cpp b/src/network/access/qrestreply.cpp new file mode 100644 index 0000000000..efff394f83 --- /dev/null +++ b/src/network/access/qrestreply.cpp @@ -0,0 +1,364 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qrestreply.h" +#include "qrestreply_p.h" + +#include <QtCore/qjsonarray.h> +#include <QtCore/qjsondocument.h> +#include <QtCore/qjsonobject.h> +#include <QtCore/qlatin1stringmatcher.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qstringconverter.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +Q_DECLARE_LOGGING_CATEGORY(lcQrest) + +/*! + \class QRestReply + \since 6.7 + \brief QRestReply is the class for following up the requests sent with + QRestAccessManager. + + \reentrant + \ingroup network + \inmodule QtNetwork + + QRestReply is a convenience class for typical RESTful client + applications. It wraps the more detailed QNetworkReply and provides + convenience methods for data and status handling. + + \sa QRestAccessManager, QNetworkReply +*/ + +/*! + \fn void QRestReply::finished(QRestReply *reply) + + This signal is emitted when \a reply has finished processing. This + signal is emitted also in cases when the reply finished due to network + or protocol errors (the server did not reply with an HTTP status). + + \sa isFinished(), httpStatus(), error() +*/ + +/*! + \fn void QRestReply::errorOccurred(QRestReply *reply) + + This signal is emitted if, while processing \a reply, an error occurs that + is considered to be a network/protocol error. These errors are + disctinct from HTTP error responses such as \c {500 Internal Server Error}. + This signal is emitted together with the + finished() signal, and often connecting to that is sufficient. + + \sa finished(), isFinished(), httpStatus(), error() +*/ + +QRestReply::QRestReply(QNetworkReply *reply, QObject *parent) + : QObject(*new QRestReplyPrivate, parent) +{ + Q_D(QRestReply); + Q_ASSERT(reply); + d->networkReply = reply; + // Reparent so that destruction of QRestReply destroys QNetworkReply + reply->setParent(this); +} + +/*! + Destroys this QRestReply object. + + \sa abort() +*/ +QRestReply::~QRestReply() + = default; + +/*! + Returns a pointer to the underlying QNetworkReply wrapped by this object. +*/ +QNetworkReply *QRestReply::networkReply() const +{ + Q_D(const QRestReply); + return d->networkReply; +} + +/*! + Aborts the network operation immediately. The finished() signal + will be emitted. + + \sa QRestAccessManager::abortRequests() QNetworkReply::abort() +*/ +void QRestReply::abort() +{ + Q_D(QRestReply); + d->networkReply->abort(); +} + +/*! + Returns the received data as a QJsonObject. Requires the reply to be + finished. + + The returned value is wrapped in \c std::optional. If the conversion + from the received data fails (empty data or JSON parsing error), + \c std::nullopt is returned. + + Calling this function consumes the received data, and any further calls + to get response data will return empty. + + This function returns \c {std::nullopt} and will not consume + any data if the reply is not finished. + + \sa jsonArray(), body(), text(), finished(), isFinished() +*/ +std::optional<QJsonObject> QRestReply::json() +{ + Q_D(QRestReply); + if (!isFinished()) { + qCWarning(lcQrest, "Attempt to read json() of an unfinished reply, ignoring."); + return std::nullopt; + } + const QJsonDocument json = d->replyDataToJson(); + return json.isObject() ? std::optional{json.object()} : std::nullopt; +} + +/*! + Returns the received data as a QJsonArray. Requires the reply to be + finished. + + The returned value is wrapped in \c std::optional. If the conversion + from the received data fails (empty data or JSON parsing error), + \c std::nullopt is returned. + + Calling this function consumes the received data, and any further calls + to get response data will return empty. + + This function returns \c {std::nullopt} and will not consume + any data if the reply is not finished. + + \sa json(), body(), text(), finished(), isFinished() +*/ +std::optional<QJsonArray> QRestReply::jsonArray() +{ + Q_D(QRestReply); + if (!isFinished()) { + qCWarning(lcQrest, "Attempt to read jsonArray() of an unfinished reply, ignoring."); + return std::nullopt; + } + const QJsonDocument json = d->replyDataToJson(); + return json.isArray() ? std::optional{json.array()} : std::nullopt; +} + +/*! + Returns the received data as a QByteArray. + + Calling this function consumes the data received so far, and any further + calls to get response data will return empty until further data has been + received. + + \sa json(), text() +*/ +QByteArray QRestReply::body() +{ + Q_D(QRestReply); + return d->networkReply->readAll(); +} + +/*! + Returns the received data as a QString. Requires the reply to be finished. + + The received data is decoded into a QString (UTF-16). The decoding + uses the \e Content-Type header's \e charset parameter to determine the + source encoding, if available. If the encoding information is not + available or not supported by \l QStringConverter, UTF-8 is used as a + default. + + Calling this function consumes the received data, and any further calls + to get response data will return empty. + + This function returns a default-constructed value and will not consume + any data if the reply is not finished. + + \sa json(), body(), isFinished(), finished() +*/ +QString QRestReply::text() +{ + Q_D(QRestReply); + if (!isFinished()) { + qCWarning(lcQrest, "Attempt to read text() of an unfinished reply, ignoring."); + return {}; + } + QByteArray data = d->networkReply->readAll(); + if (data.isEmpty()) + return {}; + + const QByteArray charset = d->contentCharset(); + QStringDecoder decoder(charset); + if (!decoder.isValid()) { // the decoder may not support the mimetype's charset + qCWarning(lcQrest, "Charset \"%s\" is not supported, defaulting to UTF-8", + charset.constData()); + decoder = QStringDecoder(QStringDecoder::Utf8); + } + return decoder(data); +} + +/*! + Returns the HTTP status received in the server response. + The value is \e 0 if not available (the status line has not been received, + yet). + + \note The HTTP status is reported as indicated by the received HTTP + response. It is possible that an error() occurs after receiving the status, + for instance due to network disconnection while receiving a long response. + These potential subsequent errors are not represented by the reported + HTTP status. + + \sa isSuccess(), hasError(), error() +*/ +int QRestReply::httpStatus() const +{ + Q_D(const QRestReply); + return d->networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); +} + +/*! + \fn bool QRestReply::isSuccess() const + + Returns whether the HTTP status is between 200..299 and no + further errors have occurred while receiving the response (for example + abrupt disconnection while receiving the body data). This function + is a convenient way to check whether the response is considered successful. + + \sa httpStatus(), hasError(), error() +*/ + +/*! + Returns whether the HTTP status is between 200..299. + + \sa isSuccess(), httpStatus(), hasError(), error() +*/ +bool QRestReply::isHttpStatusSuccess() const +{ + const int status = httpStatus(); + return status >= 200 && status < 300; +} + +/*! + Returns whether an error has occurred. This includes errors such as + network and protocol errors, but excludes cases where the server + successfully responded with an HTTP error status (for example + \c {500 Internal Server Error}). Use \l httpStatus() or + \l isHttpStatusSuccess() to get the HTTP status information. + + \sa httpStatus(), isSuccess(), error(), errorString() +*/ +bool QRestReply::hasError() const +{ + Q_D(const QRestReply); + return d->hasNonHttpError(); +} + +/*! + Returns the last error, if any. The errors include + errors such as network and protocol errors, but exclude + cases when the server successfully responded with an HTTP status. + + \sa httpStatus(), isSuccess(), hasError(), errorString() +*/ +QNetworkReply::NetworkError QRestReply::error() const +{ + Q_D(const QRestReply); + if (!hasError()) + return QNetworkReply::NetworkError::NoError; + return d->networkReply->error(); +} + +/*! + Returns a human-readable description of the last network error. + + \sa httpStatus(), isSuccess(), hasError(), error() +*/ +QString QRestReply::errorString() const +{ + Q_D(const QRestReply); + if (hasError()) + return d->networkReply->errorString(); + return {}; +} + +/*! + Returns whether the network request has finished. +*/ +bool QRestReply::isFinished() const +{ + Q_D(const QRestReply); + return d->networkReply->isFinished(); +} + +QRestReplyPrivate::QRestReplyPrivate() + = default; + +QRestReplyPrivate::~QRestReplyPrivate() + = default; + +QByteArray QRestReplyPrivate::contentCharset() const +{ + // Content-type consists of mimetype and optional parameters, of which one may be 'charset' + // Example values and their combinations below are all valid, see RFC 7231 section 3.1.1.5 + // and RFC 2045 section 5.1 + // + // text/plain; charset=utf-8 + // text/plain; charset=utf-8;version=1.7 + // text/plain; charset = utf-8 + // text/plain; charset ="utf-8" + QByteArray contentTypeValue = + networkReply->header(QNetworkRequest::KnownHeaders::ContentTypeHeader).toByteArray(); + // Default to the most commonly used UTF-8. + QByteArray charset{"UTF-8"}; + + QList<QByteArray> parameters = contentTypeValue.split(';'); + if (parameters.size() >= 2) { // Need at least one parameter in addition to the mimetype itself + parameters.removeFirst(); // Exclude the mimetype itself, only interested in parameters + QLatin1StringMatcher matcher("charset="_L1, Qt::CaseSensitivity::CaseInsensitive); + qsizetype matchIndex = -1; + for (auto ¶meter : parameters) { + // Remove whitespaces and parantheses + const QByteArray curated = parameter.replace(" ", "").replace("\"",""); + // Check for match + matchIndex = matcher.indexIn(QLatin1String(curated.constData())); + if (matchIndex >= 0) { + charset = curated.sliced(matchIndex + 8); // 8 is size of "charset=" + break; + } + } + } + return charset; +} + +// Returns true if there's an error that isn't appropriately indicated by the HTTP status +bool QRestReplyPrivate::hasNonHttpError() const +{ + const int status = networkReply->attribute(QNetworkRequest::HttpStatusCodeAttribute).toInt(); + if (status > 0) { + // The HTTP status is set upon receiving the response headers, but the + // connection might still fail later while receiving the body data. + return networkReply->error() == QNetworkReply::RemoteHostClosedError; + } + return networkReply->error() != QNetworkReply::NoError; +} + +QJsonDocument QRestReplyPrivate::replyDataToJson() +{ + QJsonParseError parseError; + const QByteArray data = networkReply->readAll(); + const QJsonDocument json = QJsonDocument::fromJson(data, &parseError); + + if (parseError.error != QJsonParseError::NoError) { + qCDebug(lcQrest) << "Response data not JSON:" << parseError.errorString() + << "at" << parseError.offset << data; + } + return json; +} + +QT_END_NAMESPACE + +#include "moc_qrestreply.cpp" diff --git a/src/network/access/qrestreply.h b/src/network/access/qrestreply.h new file mode 100644 index 0000000000..6e35b45003 --- /dev/null +++ b/src/network/access/qrestreply.h @@ -0,0 +1,55 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTREPLY_H +#define QRESTREPLY_H + +#include <QtNetwork/qnetworkreply.h> + +QT_BEGIN_NAMESPACE + +class QRestReplyPrivate; +class Q_NETWORK_EXPORT QRestReply : public QObject +{ + Q_OBJECT + +public: + ~QRestReply() override; + + QNetworkReply *networkReply() const; + + std::optional<QJsonObject> json(); + std::optional<QJsonArray> jsonArray(); + QByteArray body(); + QString text(); + + bool isSuccess() const + { + return !hasError() && isHttpStatusSuccess(); + } + int httpStatus() const; + bool isHttpStatusSuccess() const; + + bool hasError() const; + QNetworkReply::NetworkError error() const; + QString errorString() const; + + bool isFinished() const; + +public Q_SLOTS: + void abort(); + +Q_SIGNALS: + void finished(QRestReply *reply); + void errorOccurred(QRestReply *reply); + +private: + friend class QRestAccessManagerPrivate; + explicit QRestReply(QNetworkReply *reply, QObject *parent = nullptr); + Q_DECLARE_PRIVATE(QRestReply) + Q_DISABLE_COPY_MOVE(QRestReply) +}; + +QT_END_NAMESPACE + +#endif // QRESTREPLY_H diff --git a/src/network/access/qrestreply_p.h b/src/network/access/qrestreply_p.h new file mode 100644 index 0000000000..4b156b3793 --- /dev/null +++ b/src/network/access/qrestreply_p.h @@ -0,0 +1,39 @@ +// Copyright (C) 2023 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QRESTREPLY_P_H +#define QRESTREPLY_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists for the convenience +// of the Network Access API. This header file may change from +// version to version without notice, or even be removed. +// +// We mean it. +// + +#include "private/qobject_p.h" +#include <QtNetwork/qnetworkreply.h> +#include <QtCore/qjsondocument.h> + +QT_BEGIN_NAMESPACE + +class QRestReplyPrivate : public QObjectPrivate +{ +public: + QRestReplyPrivate(); + ~QRestReplyPrivate() override; + + QNetworkReply *networkReply = nullptr; + + QByteArray contentCharset() const; + bool hasNonHttpError() const; + QJsonDocument replyDataToJson(); +}; + +QT_END_NAMESPACE + +#endif |