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