RESTful API Server

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

 #include <QtGui/QColor>
 #include <QtCore/QDateTime>
 #include <QtCore/QJsonArray>
 #include <QtCore/QJsonObject>
 #include <QtCore/QJsonParseError>
 #include <QtCore/QString>
 #include <QtCore/qtypes.h>

 #include <algorithm>
 #include <optional>

 struct Jsonable
 {
     virtual QJsonObject toJson() const = 0;
     virtual ~Jsonable() = default;
 };

 struct Updatable
 {
     virtual bool update(const QJsonObject &json) = 0;
     virtual void updateFields(const QJsonObject &json) = 0;
     virtual ~Updatable() = default;
 };

 template<typename T>
 struct FromJsonFactory
 {
     virtual std::optional<T> fromJson(const QJsonObject &json) const = 0;
     virtual ~FromJsonFactory() = default;
 };

 struct User : public Jsonable, public Updatable
 {
     qint64 id;
     QString email;
     QString firstName;
     QString lastName;
     QUrl avatarUrl;
     QDateTime createdAt;
     QDateTime updatedAt;

     explicit User(const QString &email, const QString &firstName, const QString &lastName,
                   const QUrl &avatarUrl,
                   const QDateTime &createdAt = QDateTime::currentDateTimeUtc(),
                   const QDateTime &updatedAt = QDateTime::currentDateTimeUtc())
         : id(nextId()),
           email(email),
           firstName(firstName),
           lastName(lastName),
           avatarUrl(avatarUrl),
           createdAt(createdAt),
           updatedAt(updatedAt)
     {
     }

     bool update(const QJsonObject &json) override
     {
         if (!json.contains("email") || !json.contains("first_name") || !json.contains("last_name")
             || !json.contains("avatar"))
             return false;

         email = json.value("email").toString();
         firstName = json.value("first_name").toString();
         lastName = json.value("last_name").toString();
         avatarUrl.setPath(json.value("avatar").toString());
         updateTimestamp();
         return true;
     }

     void updateFields(const QJsonObject &json) override
     {
         if (json.contains("email"))
             email = json.value("email").toString();
         if (json.contains("first_name"))
             firstName = json.value("first_name").toString();
         if (json.contains("last_name"))
             lastName = json.value("last_name").toString();
         if (json.contains("avatar"))
             avatarUrl.setPath(json.value("avatar").toString());
         updateTimestamp();
     }

     QJsonObject toJson() const override
     {
         return QJsonObject{ { "id", id },
                             { "email", email },
                             { "first_name", firstName },
                             { "last_name", lastName },
                             { "avatar", avatarUrl.toString() },
                             { "createdAt", createdAt.toString(Qt::ISODateWithMs) },
                             { "updatedAt", updatedAt.toString(Qt::ISODateWithMs) } };
     }

 private:
     void updateTimestamp() { updatedAt = QDateTime::currentDateTimeUtc(); }

     static qint64 nextId()
     {
         static qint64 lastId = 1;
         return lastId++;
     }
 };

 struct UserFactory : public FromJsonFactory<User>
 {
     UserFactory(const QString &scheme, const QString &hostName, int port)
         : scheme(scheme), hostName(hostName), port(port)
     {
     }

     std::optional<User> fromJson(const QJsonObject &json) const override
     {
         if (!json.contains("email") || !json.contains("first_name") || !json.contains("last_name")
             || !json.contains("avatar")) {
             return std::nullopt;
         }

         if (json.contains("createdAt") && json.contains("updatedAt")) {
             return User(
                     json.value("email").toString(), json.value("first_name").toString(),
                     json.value("last_name").toString(), json.value("avatar").toString(),
                     QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODateWithMs),
                     QDateTime::fromString(json.value("updatedAt").toString(), Qt::ISODateWithMs));
         }
         QUrl avatarUrl(json.value("avatar").toString());
         if (!avatarUrl.isValid()) {
             avatarUrl.setPath(json.value("avatar").toString());
         }
         avatarUrl.setScheme(scheme);
         avatarUrl.setHost(hostName);
         avatarUrl.setPort(port);

         return User(json.value("email").toString(), json.value("first_name").toString(),
                     json.value("last_name").toString(), avatarUrl);
     }

 private:
     QString scheme;
     QString hostName;
     int port;
 };

 struct Color : public Jsonable, public Updatable
 {
     qint64 id;
     QString name;
     QColor color;
     QString pantone;
     QDateTime createdAt;
     QDateTime updatedAt;

     explicit Color(const QString &name, const QString &color, const QString &pantone,
                    const QDateTime &createdAt = QDateTime::currentDateTimeUtc(),
                    const QDateTime &updatedAt = QDateTime::currentDateTimeUtc())
         : id(nextId()),
           name(name),
           color(QColor(color)),
           pantone(pantone),
           createdAt(createdAt),
           updatedAt(updatedAt)
     {
     }

     QJsonObject toJson() const override
     {
         return QJsonObject{ { "id", id },
                             { "name", name },
                             { "color", color.name() },
                             { "pantone_value", pantone },
                             { "createdAt", createdAt.toString(Qt::ISODateWithMs) },
                             { "updatedAt", updatedAt.toString(Qt::ISODateWithMs) } };
     }

     bool update(const QJsonObject &json) override
     {
         if (!json.contains("name") || !json.contains("color") || !json.contains("pantone_value"))
             return false;

         name = json.value("name").toString();
         color = QColor(json.value("color").toString());
         pantone = json.value("pantone_value").toString();
         updateTimestamp();
         return true;
     }

     void updateFields(const QJsonObject &json) override
     {
         if (json.contains("name"))
             name = json.value("name").toString();
         if (json.contains("color"))
             color = QColor(json.value("color").toString());
         if (json.contains("pantone_value"))
             pantone = json.value("pantone_value").toString();
         updateTimestamp();
     }

 private:
     void updateTimestamp() { updatedAt = QDateTime::currentDateTimeUtc(); }

     static qint64 nextId()
     {
         static qint64 lastId = 1;
         return lastId++;
     }
 };

 struct ColorFactory : public FromJsonFactory<Color>
 {
     std::optional<Color> fromJson(const QJsonObject &json) const override
     {
         if (!json.contains("name") || !json.contains("color") || !json.contains("pantone_value"))
             return std::nullopt;
         if (json.contains("createdAt") && json.contains("updatedAt")) {
             return Color(
                     json.value("name").toString(), json.value("color").toString(),
                     json.value("pantone_value").toString(),
                     QDateTime::fromString(json.value("createdAt").toString(), Qt::ISODateWithMs),
                     QDateTime::fromString(json.value("updatedAt").toString(), Qt::ISODateWithMs));
         }
         return Color(json.value("name").toString(), json.value("color").toString(),
                      json.value("pantone_value").toString());
     }
 };

 struct SessionEntry : public Jsonable
 {
     qint64 id;
     QString email;
     QString password;
     std::optional<QUuid> token;

     explicit SessionEntry(const QString &email, const QString &password)
         : id(nextId()), email(email), password(password)
     {
     }

     void startSession() { token = generateToken(); }

     void endSession() { token = std::nullopt; }

     QJsonObject toJson() const override
     {
         return token
                 ? QJsonObject{ { "id", id },
                                { "token", token->toString(QUuid::StringFormat::WithoutBraces) } }
                 : QJsonObject{};
     }

     bool operator==(const QString &otherToken) const
     {
         return token && *token == QUuid::fromString(otherToken);
     }

 private:
     QUuid generateToken() { return QUuid::createUuid(); }

     static qint64 nextId()
     {
         static qint64 lastId = 1;
         return lastId++;
     }
 };

 struct SessionEntryFactory : public FromJsonFactory<SessionEntry>
 {
     std::optional<SessionEntry> fromJson(const QJsonObject &json) const override
     {
         if (!json.contains("email") || !json.contains("password"))
             return std::nullopt;

         return SessionEntry(json.value("email").toString(), json.value("password").toString());
     }
 };

 template<typename T>
 class IdMap : public QMap<qint64, T>
 {
 public:
     IdMap() = default;
     explicit IdMap(const FromJsonFactory<T> &factory, const QJsonArray &array) : QMap<qint64, T>()
     {
         for (const auto &jsonValue : array) {
             if (jsonValue.isObject()) {
                 const auto maybeT = factory.fromJson(jsonValue.toObject());
                 if (maybeT) {
                     QMap<qint64, T>::insert(maybeT->id, *maybeT);
                 }
             }
         }
     }
 };

 template<typename T>
 class Paginator : public Jsonable
 {
 public:
     static constexpr qsizetype defaultPage = 1;
     static constexpr qsizetype defaultPageSize = 6;

     explicit Paginator(const T &container, qsizetype page, qsizetype size)
     {
         const auto containerSize = container.size();
         const auto pageIndex = page - 1;
         const auto pageSize = qMin(size, containerSize);
         const auto totalPages = (containerSize % pageSize) == 0 ? (containerSize / pageSize)
                                                                 : (containerSize / pageSize) + 1;
         valid = containerSize > (pageIndex * pageSize);
         if (valid) {
             QJsonArray data;

             auto iter = container.begin();
             std::advance(iter, std::min(pageIndex * pageSize, containerSize));
             for (qsizetype i = 0; i < pageSize && iter != container.end(); ++i, ++iter) {
                 data.push_back(iter->toJson());
             }
             json = QJsonObject{ { "page", pageIndex + 1 },
                                 { "per_page", pageSize },
                                 { "total", containerSize },
                                 { "total_pages", totalPages },
                                 { "data", data } };
         } else {
             json = QJsonObject{};
         }
     }

     QJsonObject toJson() const { return json; }

     constexpr bool isValid() const { return valid; }

 private:
     QJsonObject json;
     bool valid;
 };

 #endif // TYPES_H