Пишем Rest API клиент на qt5

в 14:18, , рубрики: c++, c++11, qt, qt5

Введение

Последнее время я занимаюсь разработкой настольного Rest API клиента. Довольно большая часть работы во взаимодействие с сервером, для оптимизации обработки запросов был написан класс Requester, обладающий следующими особенностями:

  • возможность отправлять как https так и http запросы
  • использование одной функции для всех типов запросов
  • возможность получить все данные по запросу с сервера, а не одну страницу(n записей)

Программисту, использующему этот класс, придется работать тремя функциями:

    void initRequester(const QString& host, int port, QSslConfiguration *value);

    // функция посылает один запрос
    void sendRequest(const QString &apiStr,
                       const handleFunc &funcSuccess,
                       const handleFunc &funcError,
                       int type = Type::GET,
                       const QVariantMap &data = QVariantMap());

     //функция будет посылать GET запрос пока не будет достигнута последня старница
    void sendMulishGetRequest(
                       const QString &apiStr,
                       const handleFunc &funcSuccess,
                       const handleFunc &funcError,
                       const finishFunc &funcFinish);

funcSuccess — callback, вызываемый в случае, если запрос выполнился успешно
funcError — callback в случае ошибки

    typedef std::function<void(const QJsonObject &)> handleFunc;
    typedef std::function<void()> finishFunc;
    enum class Type {
        POST,
        GET,
        PATCH,
        DELET     
    };

DELET — не опечатка, так как с DELETE не собирается под WINDOWS.

Реализация

Для взаимодействия с сервером будем использовать три Qt класса: QNetworkAccessManager — реализует механизм запросов на сервер, QNetworkReply — ответ сервера на наш запрос и QNetworkRequest — собственно сам запрос.

Я не вижу смысла описывать реализацию функции initRequester, поэтому перейдём сразу к SendRequest. Идея состоит в том, что мы создаем объект класса QNetworkRequest. В зависимости от типа запроса, передаем его с дополнительными данными (тело запроса, если есть) в объект класса QNetworkAccessManager. Ответ записывается в reply(объект класса QNetworkReply). Так как запросы выполняются асинхронно, то по сигналу finished от reply будем вызывать лямбду, проверяющую были ли ошибки и вызывающую соответсвующий callback, она же занимается высвобождением ресурсов.
Для создания request`а используется следующий код:

QNetworkRequest Requester::createRequest(const QString &apiStr)
{
    QNetworkRequest request;
    QString url = pathTemplate.arg(host).arg(port).arg(apiStr);
    request.setUrl(QUrl(url));
    request.setRawHeader("Content-Type","application/json");
    // здесь прописываются все необходимые заголовки запроса 
    if (sslConfig != nullptr)
        request.setSslConfiguration(*sslConfig);

    return request;
}

А вот и код самой функции для запросов на сервер:

void Requester::sendRequest(const QString &apiStr,
                            const handleFunc &funcSuccess,
                            const handleFunc &funcError,
                            int type,
                            const QVariantMap &data)
{
    QNetworkRequest request = createRequest(apiStr);

    QNetworkReply *reply;
    switch (type) {
    case Type::POST: {
        QByteArray postDataByteArray = variantMapToJson(data);
        reply = manager->post(request, postDataByteArray);
        break;
    } case Type::GET: {
        reply = manager->get(request);
        break;
    } case Type::DELET: {
        if (data.isEmpty())
            reply = manager->deleteResource(request);
        else
            reply = sendCustomRequest(manager, request, "DELETE", data); //реализация ниже
        break;
    } case Type::PATCH: {
        reply = sendCustomRequest(manager, request, "PATCH", data);
        break;
    } default:
        reply = nullptr;
    }

    connect(reply, &QNetworkReply::finished, this, [this, funcSuccess, funcError, reply]() {
        // данная часть функции написана с учетом того, что ответ будет в формате json
        QJsonObject obj = parseReply(reply);

        if (onFinishRequest(reply)) {
            if (funcSuccess != nullptr)
                funcSuccess(obj);
        } else {
            if (funcError != nullptr) {
                handleQtNetworkErrors(reply, obj);
                funcError(obj);
            }
        }
        reply->close();
        reply->deleteLater();
    } );
}

Внимательный читатель заметил, что для некоторых запросов DELETE и всех PATCH для создания объекта QNetworkReply используется функция sendCustomRequest. Это объясняется тем, что QNetworkAccessManager не умеет из коробки посылать DELETE запросы с телом, и совсем не умеет PATCH. Для решения этой проблемы напишем небольшую функцию обертку:

QNetworkReply* Requester::sendCustomRequest(QNetworkAccessManager* manager,
                                            QNetworkRequest &request,
                                            const QString &type,
                                            const QVariantMap &data)
{
    request.setRawHeader("HTTP", type.toUtf8());
    QByteArray postDataByteArray = variantMapToJson(data);
    QBuffer *buff = new QBuffer;
    buff->setData(postDataByteArray);
    buff->open(QIODevice::ReadOnly);
    QNetworkReply* reply =  manager->sendCustomRequest(request, type.toUtf8(), buff);
    buff->setParent(reply);
    return reply;
}

Подробно рассматривать функцию sendMulishGetRequest не вижу смысла, так как она похожа на рассмотренную выше. Отличия заключаются в том, что после первого успешного выполнения запроса, из ответа будет вытащена ссылка на следующую страницу, после чего произойдет рекурсивный вызов. Если следующей страницы нет, то будет выполнена функция funcFinish.

Заключение

Получившийся в итоге, класс имеет очень простой интерфейс для использования, а так же избавляет нас от многих рутинных действий.

Автор: Bragaman

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js