diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 83f9a4c..dac6505 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -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 "$" @@ -34,13 +42,7 @@ target_include_directories(QHttpEngine PUBLIC "$" ) -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}" diff --git a/src/QHttpEngine/qobjecthandler.h b/src/QHttpEngine/qobjecthandler.h index d85d0cd..c3a8ba1 100644 --- a/src/QHttpEngine/qobjecthandler.h +++ b/src/QHttpEngine/qobjecthandler.h @@ -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 { diff --git a/src/qobjecthandler.cpp b/src/qobjecthandler.cpp index 262f3ec..a02c660 100644 --- a/src/qobjecthandler.cpp +++ b/src/qobjecthandler.cpp @@ -20,25 +20,23 @@ * IN THE SOFTWARE. */ -#include - #include #include #include +#include #include #include #include -#include +#include +#include +#include #include #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(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 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); + }); } } diff --git a/src/qobjecthandler_p.h b/src/qobjecthandler_p.h index 33db4ac..7c6a782 100644 --- a/src/qobjecthandler_p.h +++ b/src/qobjecthandler_p.h @@ -23,7 +23,6 @@ #ifndef QHTTPENGINE_QOBJECTHANDLERPRIVATE_H #define QHTTPENGINE_QOBJECTHANDLERPRIVATE_H -#include #include #include "QHttpEngine/qhttpsocket.h" @@ -37,13 +36,8 @@ public: explicit QObjectHandlerPrivate(QObjectHandler *handler); - void invokeSlot(QHttpSocket *socket, int index, QVariantMap *queryString = NULL); - - QMap map; - -private Q_SLOTS: - - void onReadChannelFinished(); + void invokeSlot(QHttpSocket *socket, int index, const QVariantMap &query); + QVariantMap convertQueryString(const QString &query); private: diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 7e90506..66bdee0 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -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} ) diff --git a/tests/TestQObjectHandler.cpp b/tests/TestQObjectHandler.cpp index 8b5fc3d..e247dbe 100644 --- a/tests/TestQObjectHandler.cpp +++ b/tests/TestQObjectHandler.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -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("method"); QTest::addColumn("path"); QTest::addColumn("data"); + QTest::addColumn("response"); QTest::addColumn("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(QHttpSocket::NotFound); QTest::newRow("invalid signature") - << QByteArray("POST") + << QByteArray("GET") << QByteArray("invalidSignature") - << data + << QByteArray("") + << QVariantMap() << static_cast(QHttpSocket::InternalServerError); - QTest::newRow("bad method") + QTest::newRow("query string") << QByteArray("GET") - << QByteArray("validSlot") - << data - << static_cast(QHttpSocket::MethodNotAllowed); + << QByteArray("validSlot?param=value") + << QByteArray("") + << QVariantMap({{"param", "value"}}) + << static_cast(QHttpSocket::OK); QTest::newRow("malformed JSON") << QByteArray("POST") << QByteArray("validSlot") << QByteArray("") + << QVariantMap() << static_cast(QHttpSocket::BadRequest); - QTest::newRow("valid slot") + QTest::newRow("valid JSON") << QByteArray("POST") << QByteArray("validSlot") << data + << map << static_cast(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()); } }