Add ability to specify method for slot handlers.
This commit is contained in:
@@ -26,7 +26,15 @@ else()
|
||||
add_library(QHttpEngine SHARED ${HEADERS} ${SRC})
|
||||
endif()
|
||||
|
||||
qt5_use_modules(QHttpEngine Network)
|
||||
set_target_properties(QHttpEngine PROPERTIES
|
||||
CXX_STANDARD 11
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
DEFINE_SYMBOL QT_NO_SIGNALS_SLOTS_KEYWORDS
|
||||
DEFINE_SYMBOL QHTTPENGINE_LIBRARY
|
||||
PUBLIC_HEADER "${HEADERS}"
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||
)
|
||||
|
||||
target_include_directories(QHttpEngine PUBLIC
|
||||
"$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}>"
|
||||
@@ -34,13 +42,7 @@ target_include_directories(QHttpEngine PUBLIC
|
||||
"$<INSTALL_INTERFACE:${INCLUDE_INSTALL_DIR}>"
|
||||
)
|
||||
|
||||
set_target_properties(QHttpEngine PROPERTIES
|
||||
DEFINE_SYMBOL QT_NO_SIGNALS_SLOTS_KEYWORDS
|
||||
DEFINE_SYMBOL QHTTPENGINE_LIBRARY
|
||||
PUBLIC_HEADER "${HEADERS}"
|
||||
VERSION ${PROJECT_VERSION}
|
||||
SOVERSION ${PROJECT_VERSION_MAJOR}
|
||||
)
|
||||
target_link_libraries(QHttpEngine Qt5::Network)
|
||||
|
||||
install(TARGETS QHttpEngine EXPORT QHttpEngine-export
|
||||
RUNTIME DESTINATION "${BIN_INSTALL_DIR}"
|
||||
|
||||
@@ -33,29 +33,31 @@ class QHTTPENGINE_EXPORT QObjectHandlerPrivate;
|
||||
* @headerfile qobjecthandler.h QHttpEngine/QObjectHandler
|
||||
*
|
||||
* This handler enables incoming requests to invoke a matching slot in a
|
||||
* QObject-derived class. The request body is expected to contain parameters
|
||||
* encoded as a JSON object. This object is then passed to the slot as a
|
||||
* single QVariantMap argument. The slot should return a QVariantMap
|
||||
* containing the response.
|
||||
* QObject-derived class. The slot name is used to determine the HTTP verb
|
||||
* that the method expects. For all requests, the query string is parsed and
|
||||
* supplied to the slot as a parameter. For POST requests, the request body is
|
||||
* expected to be a JSON object and it is supplied to the slot as a
|
||||
* QVariantMap. The slot is expected to return a QVariantMap containing the
|
||||
* response, which will be encoded as a JSON object.
|
||||
*
|
||||
* To use this class, it must be subclassed and one or more slots must be
|
||||
* created. The name of the slot will be used to determine the path. For
|
||||
* example, the following handler consists of a single method that can be
|
||||
* invoked by using the `/doSomething` path.
|
||||
* created. The name of the slot will be used to determine the HTTP method and
|
||||
* path. For example, the following handler consists of two methods that can
|
||||
* be invoked by using the `/something` path.
|
||||
*
|
||||
* @code
|
||||
* class TestHandler : public QObjectHandler
|
||||
* {
|
||||
* Q_OBJECT
|
||||
* private slots:
|
||||
* QVariantMap doSomething(QVariantMap params);
|
||||
* QVariantMap get_something(QVariantMap queryString);
|
||||
* QVariantMap post_something(QVariantMap queryString, QVariantMap parameters);
|
||||
* };
|
||||
* @endcode
|
||||
*
|
||||
* The request body must contain valid JSON which will be decoded and passed
|
||||
* to the doSomething() slot as a QVariantMap. The slot should return a
|
||||
* QVariantMap which will then be encoded as JSON and written to the socket as
|
||||
* the response body.
|
||||
* The slot name should begin with the HTTP method, followed by an underscore,
|
||||
* and finally the name of the method which will be used to determine the path
|
||||
* used to invoke it.
|
||||
*/
|
||||
class QHTTPENGINE_EXPORT QObjectHandler : public QHttpHandler
|
||||
{
|
||||
|
||||
@@ -20,25 +20,23 @@
|
||||
* IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include <iostream>
|
||||
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonParseError>
|
||||
#include <QList>
|
||||
#include <QMetaMethod>
|
||||
#include <QMetaObject>
|
||||
#include <QMetaType>
|
||||
#include <QStringList>
|
||||
#include <QPair>
|
||||
#include <QUrl>
|
||||
#include <QUrlQuery>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include "QHttpEngine/qobjecthandler.h"
|
||||
#include "qobjecthandler_p.h"
|
||||
|
||||
#define HTTP_GET "GET"
|
||||
#define HTTP_POST "POST"
|
||||
#define HTTP_PUT "PUT"
|
||||
#define HTTP_HEAD "HEAD"
|
||||
#define HTTP_CONNECT "CONNECT"
|
||||
const QString MethodGET = "GET";
|
||||
const QString MethodPOST = "POST";
|
||||
|
||||
QObjectHandlerPrivate::QObjectHandlerPrivate(QObjectHandler *handler)
|
||||
: QObject(handler),
|
||||
@@ -46,23 +44,32 @@ QObjectHandlerPrivate::QObjectHandlerPrivate(QObjectHandler *handler)
|
||||
{
|
||||
}
|
||||
|
||||
void QObjectHandlerPrivate::invokeSlot(QHttpSocket *socket, int index, QVariantMap *queryString)
|
||||
void QObjectHandlerPrivate::invokeSlot(QHttpSocket *socket, int index, const QVariantMap &query)
|
||||
{
|
||||
// Attempt to decode the JSON from the socket
|
||||
QJsonParseError error;
|
||||
QJsonDocument document = QJsonDocument::fromJson(socket->readAll(), &error);
|
||||
QGenericArgument secondArgument;
|
||||
|
||||
// Ensure that the document is valid
|
||||
if(error.error != QJsonParseError::NoError && socket->method() != HTTP_GET) {
|
||||
socket->writeError(QHttpSocket::BadRequest);
|
||||
return;
|
||||
// If this is a POST request, then decode the request body
|
||||
if (socket->method() == MethodPOST) {
|
||||
|
||||
// Attempt to decode the JSON from the socket
|
||||
QJsonParseError error;
|
||||
QJsonDocument document = QJsonDocument::fromJson(socket->readAll(), &error);
|
||||
|
||||
// Ensure that the document is valid
|
||||
if (error.error != QJsonParseError::NoError) {
|
||||
socket->writeError(QHttpSocket::BadRequest);
|
||||
return;
|
||||
}
|
||||
|
||||
secondArgument = Q_ARG(QVariantMap, document.object().toVariantMap());
|
||||
}
|
||||
|
||||
// Attempt to invoke the slot
|
||||
QVariantMap retVal;
|
||||
if(!q->metaObject()->method(index).invoke(q,
|
||||
if (!q->metaObject()->method(index).invoke(q,
|
||||
Q_RETURN_ARG(QVariantMap, retVal),
|
||||
Q_ARG(QVariantMap, (queryString != NULL ? *queryString : document.object().toVariantMap())))) {
|
||||
Q_ARG(QVariantMap, query),
|
||||
secondArgument)) {
|
||||
socket->writeError(QHttpSocket::InternalServerError);
|
||||
return;
|
||||
}
|
||||
@@ -75,16 +82,14 @@ void QObjectHandlerPrivate::invokeSlot(QHttpSocket *socket, int index, QVariantM
|
||||
socket->close();
|
||||
}
|
||||
|
||||
void QObjectHandlerPrivate::onReadChannelFinished()
|
||||
QVariantMap QObjectHandlerPrivate::convertQueryString(const QString &query)
|
||||
{
|
||||
// Obtain the pointer to the socket emitting the signal
|
||||
QHttpSocket *socket = qobject_cast<QHttpSocket*>(sender());
|
||||
|
||||
// Obtain the index and remove it from the map
|
||||
int index = map.take(socket);
|
||||
|
||||
// Actually invoke the slot
|
||||
invokeSlot(socket, index);
|
||||
QVariantMap map;
|
||||
QPair<QString, QString> pair;
|
||||
foreach (pair, QUrlQuery(query).queryItems()) {
|
||||
map.insert(pair.first, pair.second);
|
||||
}
|
||||
return map;
|
||||
}
|
||||
|
||||
QObjectHandler::QObjectHandler(QObject *parent)
|
||||
@@ -95,55 +100,47 @@ QObjectHandler::QObjectHandler(QObject *parent)
|
||||
|
||||
void QObjectHandler::process(QHttpSocket *socket, const QString &path)
|
||||
{
|
||||
// Only GET | POST requests are accepted - reject any other methods but ensure
|
||||
// that the Allow header is set in order to comply with RFC 2616
|
||||
if(socket->method() != HTTP_POST && socket->method() != HTTP_GET) {
|
||||
socket->setHeader("Allow", HTTP_POST);
|
||||
socket->setHeader("Allow", HTTP_GET);
|
||||
socket->writeError(QHttpSocket::MethodNotAllowed);
|
||||
return;
|
||||
}
|
||||
QStringList pathSplit = path.split("?");
|
||||
QVariantMap queryString;
|
||||
if(socket->method() == HTTP_GET && pathSplit.length() > 1) {
|
||||
QStringList qstr = pathSplit[1].split("&");
|
||||
foreach(QString q, qstr) {
|
||||
QStringList kv = q.split("=");
|
||||
if(kv.length() != 2) break;
|
||||
queryString[kv[0]] = kv[1];
|
||||
}
|
||||
}
|
||||
QString methodName = socket->method().toLower() + "_" + pathSplit[0];
|
||||
// Determine the correct slot name to invoke based on the HTTP method and
|
||||
// the path that was provided - note that the path needs to be split to
|
||||
// remove the query string
|
||||
QUrl url(path);
|
||||
QVariantMap query = d->convertQueryString(url.query());
|
||||
QString slotName = QString("%1_%2")
|
||||
.arg(QString(socket->method().toLower()))
|
||||
.arg(url.path());
|
||||
|
||||
// Determine the index of the slot with the specified name - note that we
|
||||
// don't need to worry about retrieving the index for deleteLater() since
|
||||
// we specify the "QVariantMap" parameter type, which no parent slots use
|
||||
int index = metaObject()->indexOfSlot(QString("%1(QVariantMap)").arg(methodName).toUtf8().data());
|
||||
// Determine the parameters the slot should have based on the method
|
||||
QString parameters;
|
||||
if (socket->method() == MethodGET) {
|
||||
parameters = "QVariantMap";
|
||||
} else {
|
||||
parameters = "QVariantMap,QVariantMap";
|
||||
}
|
||||
|
||||
// Attempt to find the method's index so we can invoke it later
|
||||
int index = metaObject()->indexOfSlot(QString("%1(%2)").arg(slotName).arg(parameters).toUtf8().data());
|
||||
|
||||
// If the index is invalid, the "resource" was not found
|
||||
if(index == -1) {
|
||||
// TODO: an HTTP 405 should be returned when the method is incorrect
|
||||
if (index == -1) {
|
||||
socket->writeError(QHttpSocket::NotFound);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that the return type of the slot is QVariantMap
|
||||
QMetaMethod method = metaObject()->method(index);
|
||||
if(method.returnType() != QMetaType::QVariantMap) {
|
||||
qCritical()<< "Return type is not valid!!!";
|
||||
if (method.returnType() != QMetaType::QVariantMap) {
|
||||
socket->writeError(QHttpSocket::InternalServerError);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check to see if the socket has finished receiving all of the data yet
|
||||
// or not - if so, jump to invokeSlot(), otherwise wait for the
|
||||
// readChannelFinished() signal
|
||||
if(socket->bytesAvailable() >= socket->contentLength()) {
|
||||
d->invokeSlot(socket, index, socket->method() == HTTP_GET ? &queryString : NULL);
|
||||
// If the slot has finished receiving all of the data, jump directly to
|
||||
// invokeSlot(), otherwise, wait until we have the rest of it
|
||||
if (socket->bytesAvailable() >= socket->contentLength()) {
|
||||
d->invokeSlot(socket, index, query);
|
||||
} else {
|
||||
|
||||
// Add the socket and index to the map so that the latter can be
|
||||
// retrieved when the readChannelFinished() signal is emitted
|
||||
d->map.insert(socket, index);
|
||||
connect(socket, SIGNAL(readChannelFinished()), d, SLOT(onReadChannelFinished()));
|
||||
connect(socket, &QHttpSocket::readChannelFinished, [this, socket, index, &query]() {
|
||||
d->invokeSlot(socket, index, query);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -23,7 +23,6 @@
|
||||
#ifndef QHTTPENGINE_QOBJECTHANDLERPRIVATE_H
|
||||
#define QHTTPENGINE_QOBJECTHANDLERPRIVATE_H
|
||||
|
||||
#include <QMap>
|
||||
#include <QObject>
|
||||
|
||||
#include "QHttpEngine/qhttpsocket.h"
|
||||
@@ -37,13 +36,8 @@ public:
|
||||
|
||||
explicit QObjectHandlerPrivate(QObjectHandler *handler);
|
||||
|
||||
void invokeSlot(QHttpSocket *socket, int index, QVariantMap *queryString = NULL);
|
||||
|
||||
QMap<QObject*, int> map;
|
||||
|
||||
private Q_SLOTS:
|
||||
|
||||
void onReadChannelFinished();
|
||||
void invokeSlot(QHttpSocket *socket, int index, const QVariantMap &query);
|
||||
QVariantMap convertQueryString(const QString &query);
|
||||
|
||||
private:
|
||||
|
||||
|
||||
@@ -16,8 +16,11 @@ set(TESTS
|
||||
|
||||
foreach(TEST ${TESTS})
|
||||
add_executable(${TEST} ${TEST}.cpp)
|
||||
qt5_use_modules(${TEST} Test)
|
||||
target_link_libraries(${TEST} QHttpEngine common)
|
||||
set_target_properties(${TEST} PROPERTIES
|
||||
CXX_STANDARD 11
|
||||
CXX_STANDARD_REQUIRED ON
|
||||
)
|
||||
target_link_libraries(${TEST} Qt5::Test QHttpEngine common)
|
||||
add_test(NAME ${TEST}
|
||||
COMMAND ${TEST}
|
||||
)
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include <QJsonObject>
|
||||
#include <QObject>
|
||||
#include <QTest>
|
||||
#include <QUrlQuery>
|
||||
#include <QVariantMap>
|
||||
|
||||
#include <QHttpEngine/QHttpSocket>
|
||||
@@ -38,8 +39,11 @@ class DummyHandler : public QObjectHandler
|
||||
|
||||
private Q_SLOTS:
|
||||
|
||||
void invalidSignature(QVariantMap) {}
|
||||
QVariantMap validSlot(QVariantMap params) {
|
||||
void get_invalidSignature(QVariantMap) {}
|
||||
QVariantMap get_validSlot(QVariantMap query) {
|
||||
return query;
|
||||
}
|
||||
QVariantMap post_validSlot(QVariantMap, QVariantMap params) {
|
||||
return params;
|
||||
}
|
||||
};
|
||||
@@ -59,42 +63,49 @@ void TestQObjectHandler::testRequests_data()
|
||||
QTest::addColumn<QByteArray>("method");
|
||||
QTest::addColumn<QByteArray>("path");
|
||||
QTest::addColumn<QByteArray>("data");
|
||||
QTest::addColumn<QVariantMap>("response");
|
||||
QTest::addColumn<int>("statusCode");
|
||||
|
||||
QVariantMap map;
|
||||
map.insert("param1", 1);
|
||||
map.insert("param2", 2);
|
||||
QVariantMap map({
|
||||
{ "param1", 1 },
|
||||
{ "param2", 2 }
|
||||
});
|
||||
|
||||
QByteArray data = QJsonDocument(QJsonObject::fromVariantMap(map)).toJson();
|
||||
|
||||
QTest::newRow("nonexistent slot")
|
||||
<< QByteArray("POST")
|
||||
<< QByteArray("GET")
|
||||
<< QByteArray("nonexistent")
|
||||
<< data
|
||||
<< QByteArray("")
|
||||
<< QVariantMap()
|
||||
<< static_cast<int>(QHttpSocket::NotFound);
|
||||
|
||||
QTest::newRow("invalid signature")
|
||||
<< QByteArray("POST")
|
||||
<< QByteArray("GET")
|
||||
<< QByteArray("invalidSignature")
|
||||
<< data
|
||||
<< QByteArray("")
|
||||
<< QVariantMap()
|
||||
<< static_cast<int>(QHttpSocket::InternalServerError);
|
||||
|
||||
QTest::newRow("bad method")
|
||||
QTest::newRow("query string")
|
||||
<< QByteArray("GET")
|
||||
<< QByteArray("validSlot")
|
||||
<< data
|
||||
<< static_cast<int>(QHttpSocket::MethodNotAllowed);
|
||||
<< QByteArray("validSlot?param=value")
|
||||
<< QByteArray("")
|
||||
<< QVariantMap({{"param", "value"}})
|
||||
<< static_cast<int>(QHttpSocket::OK);
|
||||
|
||||
QTest::newRow("malformed JSON")
|
||||
<< QByteArray("POST")
|
||||
<< QByteArray("validSlot")
|
||||
<< QByteArray("")
|
||||
<< QVariantMap()
|
||||
<< static_cast<int>(QHttpSocket::BadRequest);
|
||||
|
||||
QTest::newRow("valid slot")
|
||||
QTest::newRow("valid JSON")
|
||||
<< QByteArray("POST")
|
||||
<< QByteArray("validSlot")
|
||||
<< data
|
||||
<< map
|
||||
<< static_cast<int>(QHttpSocket::OK);
|
||||
}
|
||||
|
||||
@@ -103,6 +114,7 @@ void TestQObjectHandler::testRequests()
|
||||
QFETCH(QByteArray, method);
|
||||
QFETCH(QByteArray, path);
|
||||
QFETCH(QByteArray, data);
|
||||
QFETCH(QVariantMap, response);
|
||||
QFETCH(int, statusCode);
|
||||
|
||||
DummyHandler handler;
|
||||
@@ -125,10 +137,10 @@ void TestQObjectHandler::testRequests()
|
||||
|
||||
QTRY_COMPARE(client.statusCode(), statusCode);
|
||||
|
||||
if(statusCode == QHttpSocket::OK) {
|
||||
if (statusCode == QHttpSocket::OK) {
|
||||
QVERIFY(client.headers().contains("Content-Length"));
|
||||
QTRY_COMPARE(client.data().length(), client.headers().value("Content-Length").toInt());
|
||||
QCOMPARE(QJsonDocument::fromJson(client.data()), QJsonDocument::fromJson(data));
|
||||
QCOMPARE(QJsonObject::fromVariantMap(response), QJsonDocument::fromJson(client.data()).object());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user