RESTful API Server

 // Copyright (C) 2022 The Qt Company Ltd.
 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause
 #ifndef APIBEHAVIOR_H
 #define APIBEHAVIOR_H

 #include "types.h"
 #include "utils.h"

 #include <QtHttpServer/QHttpServer>
 #include <QtConcurrent/qtconcurrentrun.h>

 #include <optional>

 template<typename T, typename = void>
 class CrudApi
 {
 };

 template<typename T>
 class CrudApi<T,
               std::enable_if_t<std::conjunction_v<std::is_base_of<Jsonable, T>,
                                                   std::is_base_of<Updatable, T>>>>
 {
 public:
     explicit CrudApi(const IdMap<T> &data, std::unique_ptr<FromJsonFactory<T>> factory)
         : data(data), factory(std::move(factory))
     {
     }

     QFuture<QHttpServerResponse> getPaginatedList(const QHttpServerRequest &request) const
     {
         using PaginatorType = Paginator<IdMap<T>>;
         std::optional<qsizetype> maybePage;
         std::optional<qsizetype> maybePerPage;
         std::optional<qint64> maybeDelay;
         if (request.query().hasQueryItem("page"))
             maybePage = request.query().queryItemValue("page").toLongLong();
         if (request.query().hasQueryItem("per_page"))
             maybePerPage = request.query().queryItemValue("per_page").toLongLong();
         if (request.query().hasQueryItem("delay"))
             maybeDelay = request.query().queryItemValue("delay").toLongLong();

         if ((maybePage && *maybePage < 1) || (maybePerPage && *maybePerPage < 1)) {
             return QtConcurrent::run([]() {
                 return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);
             });
         }

         PaginatorType paginator(data, maybePage ? *maybePage : PaginatorType::defaultPage,
                                 maybePerPage ? *maybePerPage : PaginatorType::defaultPageSize);

         return QtConcurrent::run([paginator = std::move(paginator), maybeDelay]() {
             if (maybeDelay)
                 QThread::sleep(*maybeDelay);
             return paginator.isValid()
                     ? QHttpServerResponse(paginator.toJson())
                     : QHttpServerResponse(QHttpServerResponder::StatusCode::NoContent);
         });
     }

     QHttpServerResponse getItem(qint64 itemId) const
     {
         const auto item = data.find(itemId);
         return item != data.end() ? QHttpServerResponse(item->toJson())
                                   : QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound);
     }

     QHttpServerResponse postItem(const QHttpServerRequest &request)
     {
         const std::optional<QJsonObject> json = byteArrayToJsonObject(request.body());
         if (!json)
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);

         const std::optional<T> item = factory->fromJson(*json);
         if (!item)
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);
         if (data.contains(item->id))
             return QHttpServerResponse(QHttpServerResponder::StatusCode::AlreadyReported);

         const auto entry = data.insert(item->id, *item);
         return QHttpServerResponse(entry->toJson(), QHttpServerResponder::StatusCode::Created);
     }

     QHttpServerResponse updateItem(qint64 itemId, const QHttpServerRequest &request)
     {
         const std::optional<QJsonObject> json = byteArrayToJsonObject(request.body());
         if (!json)
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);

         auto item = data.find(itemId);
         if (item == data.end())
             return QHttpServerResponse(QHttpServerResponder::StatusCode::NoContent);
         if (!item->update(*json))
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);

         return QHttpServerResponse(item->toJson());
     }

     QHttpServerResponse updateItemFields(qint64 itemId, const QHttpServerRequest &request)
     {
         const std::optional<QJsonObject> json = byteArrayToJsonObject(request.body());
         if (!json)
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);

         auto item = data.find(itemId);
         if (item == data.end())
             return QHttpServerResponse(QHttpServerResponder::StatusCode::NoContent);
         item->updateFields(*json);

         return QHttpServerResponse(item->toJson());
     }

     QHttpServerResponse deleteItem(qint64 itemId)
     {
         if (!data.remove(itemId))
             return QHttpServerResponse(QHttpServerResponder::StatusCode::NoContent);
         return QHttpServerResponse(QHttpServerResponder::StatusCode::Ok);
     }

 private:
     IdMap<T> data;
     std::unique_ptr<FromJsonFactory<T>> factory;
 };

 class SessionApi
 {
 public:
     explicit SessionApi(const IdMap<SessionEntry> &sessions,
                         std::unique_ptr<FromJsonFactory<SessionEntry>> factory)
         : sessions(sessions), factory(std::move(factory))
     {
     }

     QHttpServerResponse registerSession(const QHttpServerRequest &request)
     {
         const auto json = byteArrayToJsonObject(request.body());
         if (!json)
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);
         const auto item = factory->fromJson(*json);
         if (!item)
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);

         const auto session = sessions.insert(item->id, *item);
         session->startSession();
         return QHttpServerResponse(session->toJson());
     }

     QHttpServerResponse login(const QHttpServerRequest &request)
     {
         const auto json = byteArrayToJsonObject(request.body());

         if (!json || !json->contains("email") || !json->contains("password"))
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);

         auto maybeSession = std::find_if(
                 sessions.begin(), sessions.end(),
                 [email = json->value("email").toString(),
                  password = json->value("password").toString()](const auto &it) {
                     return it.password == password && it.email == email;
                 });
         if (maybeSession == sessions.end()) {
             return QHttpServerResponse(QHttpServerResponder::StatusCode::NotFound);
         }
         maybeSession->startSession();
         return QHttpServerResponse(maybeSession->toJson());
     }

     QHttpServerResponse logout(const QHttpServerRequest &request)
     {
         const auto maybeToken = getTokenFromRequest(request);
         if (!maybeToken)
             return QHttpServerResponse(QHttpServerResponder::StatusCode::BadRequest);

         auto maybeSession = std::find(sessions.begin(), sessions.end(), *maybeToken);
         if (maybeSession != sessions.end())
             maybeSession->endSession();
         return QHttpServerResponse(QHttpServerResponder::StatusCode::Ok);
     }

     bool authorize(const QHttpServerRequest &request) const
     {
         const auto maybeToken = getTokenFromRequest(request);
         if (maybeToken) {
             const auto maybeSession = std::find(sessions.begin(), sessions.end(), *maybeToken);
             return maybeSession != sessions.end() && *maybeSession == *maybeToken;
         }
         return false;
     }

 private:
     IdMap<SessionEntry> sessions;
     std::unique_ptr<FromJsonFactory<SessionEntry>> factory;
 };

 #endif // APIBEHAVIOR_H