Program Listing for File archive.h

Return to documentation for file (portal/serialization/archive.h)

//
// Copyright © 2025 Jonatan Nevo.
// Distributed under the MIT license (see LICENSE file).
//

#pragma once
#include <concepts>
#include <filesystem>
#include <string>
#include <variant>

#include <llvm/ADT/StringMap.h>
#include <glaze/core/reflect.hpp>
#include <portal/core/buffer.h>
#include <portal/core/reflection/property.h>
#include <portal/core/reflection/property_concepts.h>

#include "portal/core/strings/string_id.h"
#include "portal/core/strings/string_utils.h"
#include "portal/serialization/archive.h"


namespace portal
{
class ArchiveObject;

template <typename T>
struct Archivable;

template <typename T>
concept ArchiveableConcept = requires(T t, ArchiveObject& s) {
    { t.archive(s) } -> std::same_as<void>;
};

template <typename T>
concept DearchiveableConcept = requires(T t, ArchiveObject& d) {
    { T::dearchive(d) } -> std::same_as<T>;
};

template <typename T>
concept ExternalArchiveableConcept = requires(const T& t, ArchiveObject& ar) {
    { Archivable<std::remove_cvref_t<T>>::archive(t, ar) } -> std::same_as<void>;
};

template <typename T>
concept ExternalDearchiveableConcept = requires(ArchiveObject& ar) {
    { Archivable<std::remove_cvref_t<T>>::dearchive(ar) } -> std::same_as<T>;
};

class ArchiveObject
{
public:
    using PropertyName = std::string_view;
    virtual ~ArchiveObject() = default;

    ArchiveObject() = default;
    ArchiveObject(const ArchiveObject& other) = default;
    ArchiveObject(ArchiveObject&& other) noexcept = default;
    ArchiveObject& operator=(const ArchiveObject& other) = default;
    ArchiveObject& operator=(ArchiveObject&& other) = default;

    void update(const ArchiveObject& other);

    template <typename T>
        requires(std::integral<T> || std::floating_point<T>) && (!std::is_same_v<T, bool>)
    void add_property(const PropertyName& name, const T& t)
    {
        add_property_to_map(name, {Buffer::create<T>(t), reflection::get_property_type<T>(), reflection::PropertyContainerType::scalar, 1});
    }

    template <reflection::SmallVector T>
    void add_property(const PropertyName& name, const T& t)
    {
        using ValueT = typename T::ValueParamT;
        if constexpr (ArchiveableConcept<ValueT> || ExternalArchiveableConcept<ValueT>)
        {
            Buffer buffer = Buffer::allocate(t.size() * sizeof(ArchiveObject));
            for (size_t i = 0; i < t.size(); ++i)
            {
                auto* object = new (buffer.as<ArchiveObject*>() + i) ArchiveObject();
                if constexpr (ArchiveableConcept<ValueT>)
                    t[i].archive(*object);
                else
                    Archivable<ValueT>::archive(t[i], *object);
            }

            add_property_to_map(name, {std::move(buffer), reflection::PropertyType::object, reflection::PropertyContainerType::array, t.size()});
        }
        else
        {
            Buffer buffer = Buffer::allocate(t.size() * sizeof(ArchiveObject));
            for (size_t i = 0; i < t.size(); ++i)
            {
                auto* object = new (buffer.as<ArchiveObject*>() + i) ArchiveObject();
                object->add_property("v", t[i]);
            }

            constexpr auto property_type = (reflection::String<ValueT>) ? reflection::PropertyType::null_term_string
                                                                        : reflection::get_property_type<ValueT>();

            add_property_to_map(name, {std::move(buffer), property_type, reflection::PropertyContainerType::array, t.size()});
        }
    }

    template <reflection::Vector T>
    void add_property(const PropertyName& name, const T& t)
    {
        using ValueT = typename T::value_type;
        if constexpr (ArchiveableConcept<ValueT> || ExternalArchiveableConcept<ValueT>)
        {
            Buffer buffer = Buffer::allocate(t.size() * sizeof(ArchiveObject));
            for (size_t i = 0; i < t.size(); ++i)
            {
                auto* object = new (buffer.as<ArchiveObject*>() + i) ArchiveObject();
                if constexpr (ArchiveableConcept<ValueT>)
                    t[i].archive(*object);
                else
                    Archivable<ValueT>::archive(t[i], *object);
            }

            add_property_to_map(name, {std::move(buffer), reflection::PropertyType::object, reflection::PropertyContainerType::array, t.size()});
        }
        else
        {
            Buffer buffer = Buffer::allocate(t.size() * sizeof(ArchiveObject));
            for (size_t i = 0; i < t.size(); ++i)
            {
                auto* object = new (buffer.as<ArchiveObject*>() + i) ArchiveObject();
                object->add_property("v", t[i]);
            }

            constexpr auto property_type = (reflection::String<ValueT>) ? reflection::PropertyType::null_term_string
                                                                        : reflection::get_property_type<ValueT>();

            add_property_to_map(name, {std::move(buffer), property_type, reflection::PropertyContainerType::array, t.size()});
        }
    }

    template <reflection::String T>
    void add_property(const PropertyName& name, const T& t)
    {
        add_property_to_map(
            name,
            {Buffer::copy(t.data(), t.size() + 1),
             reflection::PropertyType::character,
             reflection::PropertyContainerType::null_term_string,
             t.size() + 1});
    }

    template <reflection::GlmVec1 T>
    void add_property(const PropertyName& name, const T& t)
    {
        add_property_to_map(
            name, {Buffer::create<T>(t), reflection::get_property_type<typename T::value_type>(), reflection::PropertyContainerType::vector, 1});
    }

    template <reflection::GlmVec2 T>
    void add_property(const PropertyName& name, const T& t)
    {
        add_property_to_map(
            name, {Buffer::create<T>(t), reflection::get_property_type<typename T::value_type>(), reflection::PropertyContainerType::vector, 2});
    }

    template <reflection::GlmVec3 T>
    void add_property(const PropertyName& name, const T& t)
    {
        add_property_to_map(
            name, {Buffer::create<T>(t), reflection::get_property_type<typename T::value_type>(), reflection::PropertyContainerType::vector, 3});
    }

    template <reflection::GlmVec4 T>
    void add_property(const PropertyName& name, const T& t)
    {
        add_property_to_map(
            name, {Buffer::create<T>(t), reflection::get_property_type<typename T::value_type>(), reflection::PropertyContainerType::vector, 4});
    }

    template <reflection::Map T>
        requires std::is_convertible_v<typename T::key_type, PropertyName>
    void add_property(const PropertyName& name, const T& t)
    {
        auto* child = create_child(name);
        for (auto& [key, value] : t)
        {
            child->add_property(key, value);
        }
    }

    template <typename T>
        requires std::is_enum_v<T>
    void add_property(const PropertyName& name, const T& e)
    {
        add_property(name, portal::to_string(e));
    }

    void add_property(const PropertyName& name, const char* t);

    void add_property(const PropertyName& name, const std::filesystem::path& t);

    void add_property(const PropertyName& name, const StringId& string_id);

    void add_binary_block(const PropertyName& name, const std::vector<std::byte>& data) { add_binary_block(name, {data.data(), data.size()}); }

    void add_binary_block(const PropertyName& name, const Buffer& buffer)
    {
        add_property_to_map(name, {Buffer::copy(buffer), reflection::PropertyType::binary, reflection::PropertyContainerType::array, buffer.size});
    }

    template <ArchiveableConcept T>
    void add_property(const PropertyName& name, const T& t)
    {
        auto* child = create_child(name);
        t.archive(*child);
    }

    template <ExternalArchiveableConcept T>
    void add_property(const PropertyName& name, const T& t)
    {
        auto* child = create_child(name);
        Archivable<std::remove_cvref_t<T>>::archive(t, *child);
    }

    template <typename T>
    void add_property(const PropertyName& name, const T& t)
    {
        constexpr auto N = glz::reflect<T>::size;
        if constexpr (N > 0)
        {
            auto* child = create_child(name);

            glz::for_each<N>(
                [&]<size_t I>()
                {
                    child->add_property(glz::reflect<T>::keys[I], glz::get_member(t, glz::get<I>(glz::to_tie(t))));
                }
            );
        }
    }

    template <typename T>
        requires std::is_enum_v<T>
    bool get_property(const PropertyName& name, T& out)
    {
        std::string out_string;
        if (!get_property<std::string>(name, out_string))
            return false;

        out = portal::from_string<T>(out_string);
        return true;
    }

    template <typename T>
        requires(std::integral<T>) && (!std::is_same_v<T, bool>)
    bool get_property(const PropertyName& name, T& out)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;

        out = *property.value.as<T*>();
        return true;
    }

    template <typename T>
        requires std::floating_point<T>
    bool get_property(const PropertyName& name, T& out)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;

        if (property.type != reflection::get_property_type<T>())
        {
            if constexpr (std::is_same_v<T, float>)
            {
                out = static_cast<T>(*property.value.as<double*>());
            }
            else if constexpr (std::is_same_v<T, double>)
            {
                out = static_cast<T>(*property.value.as<float*>());
            }
            else
            {
                LOG_ERROR_TAG("Serialization", "Property {} type mismatch", name);
                return false;
            }
        }
        else
        {
            out = *property.value.as<T*>();
        }
        return true;
    }

    template <reflection::Vector T>
    bool get_property(const PropertyName& name, T& out)
    {
        out.clear();
        const auto& prop = get_property_from_map(name);
        if (prop.container_type == reflection::PropertyContainerType::invalid)
            return false;

        PORTAL_ASSERT(prop.container_type == reflection::PropertyContainerType::array, "Property {} container type mismatch", name);

        return format_array<T, typename T::value_type>(name, prop, out);
    }

    template <reflection::SmallVector T>
    bool get_property(const PropertyName& name, T& out)
    {
        out.clear();
        const auto& prop = get_property_from_map(name);
        if (prop.container_type == reflection::PropertyContainerType::invalid)
            return false;

        PORTAL_ASSERT(prop.container_type == reflection::PropertyContainerType::array, "Property {} container type mismatch", name);

        return format_array<T, typename T::ValueParamT>(name, prop, out);
    }

    template <reflection::String T>
    bool get_property(const PropertyName& name, T& out)
    {
        const auto& [value, type, container_type, elements_number] = get_property_from_map(name);
        if (type == reflection::PropertyType::invalid)
            return false;

        size_t string_length;
        if (container_type == reflection::PropertyContainerType::null_term_string)
            string_length = elements_number - 1;
        else if (container_type == reflection::PropertyContainerType::string)
            string_length = elements_number;
        else
        {
            LOG_ERROR_TAG("Serialization", "Property {} container type mismatch", name);
            string_length = 0;
        }

        const auto* data = value.as<const typename T::value_type*>();
        out = T(data, string_length);

        return true;
    }

    template <reflection::GlmVec1 T>
    bool get_property(const PropertyName& name, T& out)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;

        PORTAL_ASSERT(property.container_type == reflection::PropertyContainerType::vector, "Property {} container type mismatch", name);
        PORTAL_ASSERT(property.elements_number == 1, "Property {} elements number mismatch", name);

        out = T(*property.value.as<T*>());
        return true;
    }

    template <reflection::GlmVec2 T>
    bool get_property(const PropertyName& name, T& out)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;
        PORTAL_ASSERT(property.container_type == reflection::PropertyContainerType::vector, "Property {} container type mismatch", name);
        PORTAL_ASSERT(property.elements_number == 2, "Property {} elements number mismatch", name);

        out = T(*property.value.as<T*>());
        return true;
    }

    template <reflection::GlmVec3 T>
    bool get_property(const PropertyName& name, T& out)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;
        PORTAL_ASSERT(property.container_type == reflection::PropertyContainerType::vector, "Property {} container type mismatch", name);
        PORTAL_ASSERT(property.elements_number == 3, "Property {} elements number mismatch", name);

        out = T(*property.value.as<T*>());
        return true;
    }

    template <reflection::GlmVec4 T>
    bool get_property(const PropertyName& name, T& out)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;
        PORTAL_ASSERT(property.container_type == reflection::PropertyContainerType::vector, "Property {} container type mismatch", name);
        PORTAL_ASSERT(property.elements_number == 4, "Property {} elements number mismatch", name);

        out = T(*property.value.as<T*>());
        return true;
    }

    template <reflection::Map T>
        requires std::is_convertible_v<typename T::key_type, PropertyName>
    bool get_property(const PropertyName& name, T& out)
    {
        using ValueType = T::mapped_type;
        out.clear();
        auto* child = get_object(name);
        if (!child)
            return false;

        for (const auto& [key, property] : child->property_map)
        {
            if constexpr (DearchiveableConcept<typename T::mapped_type>)
            {
                auto* value_child = child->get_object(key);
                if (value_child)
                {
                    out[std::string(key)] = ValueType::dearchive(*value_child);
                }
            }
            else
            {
                out[std::string(key)] = *property.value.as<typename T::mapped_type*>();
            }
        }
        return true;
    }

    template <ArchiveableConcept T>
    bool get_property(const PropertyName& name, T& out)
    {
        auto* child = get_object(name);
        if (!child)
            return false;

        out = T::dearchive(*child);
        return true;
    }

    template <ExternalDearchiveableConcept T>
    bool get_property(const PropertyName& name, T& out)
    {
        auto* child = get_object(name);
        if (!child)
            return false;

        out = Archivable<std::remove_cvref_t<T>>::dearchive(*child);
        return true;
    }

    bool get_property(const PropertyName& name, std::filesystem::path& out);

    bool get_property(const PropertyName& name, StringId& out);

    bool get_binary_block(const PropertyName& name, Buffer& buffer)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;
        PORTAL_ASSERT(property.type == reflection::PropertyType::binary, "Property {} type mismatch", name);
        PORTAL_ASSERT(property.container_type == reflection::PropertyContainerType::array, "Property {} container type mismatch", name);

        buffer = Buffer::copy(property.value);
        return true;
    }

    bool get_binary_block(const PropertyName& name, std::vector<std::byte>& data)
    {
        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;
        PORTAL_ASSERT(property.type == reflection::PropertyType::binary, "Property {} type mismatch", name);
        PORTAL_ASSERT(property.container_type == reflection::PropertyContainerType::array, "Property {} container type mismatch", name);

        data.assign(property.value.as<const std::byte*>(), property.value.as<const std::byte*>() + property.value.size);
        return true;
    }

    template <typename T>
    bool get_property(const PropertyName& name, T& t)
    {
        constexpr auto N = glz::reflect<T>::size;
        if constexpr (N > 0)
        {
            auto* child = get_object(name);
            if (child == nullptr)
                return false;

            glz::for_each<N>(
                [&]<size_t I>()
                {
                    child->get_property(glz::reflect<T>::keys[I], glz::get_member(t, glz::get<I>(glz::to_tie(t))));
                }
            );

            return true;
        }
        else
        {
            return false;
        }
    }

    virtual ArchiveObject* create_child(PropertyName name);

    virtual ArchiveObject* get_object(PropertyName name) const;

    // Iterator support for range-based for loops
    auto begin() { return property_map.begin(); }
    auto end() { return property_map.end(); }
    auto begin() const { return property_map.begin(); }
    auto end() const { return property_map.end(); }

protected:
    template <typename T, typename ValueType>
        requires(reflection::Vector<T> || reflection::SmallVector<T>)
    bool format_array(const PropertyName& name, const reflection::Property& prop, T& out) const
    {
        const auto& [value, type, container_type, elements_number] = prop;

        if constexpr (ArchiveableConcept<ValueType> || ExternalDearchiveableConcept<ValueType>)
        {
            auto* objects = value.as<ArchiveObject*>();
            for (size_t i = 0; i < elements_number; ++i)
            {
                if constexpr (ArchiveableConcept<ValueType>)
                    out.push_back(ValueType::dearchive(objects[i]));
                else
                    out.push_back(Archivable<ValueType>::dearchive(objects[i]));
            }
        }
        else
        {
            auto* objects = value.as<ArchiveObject*>();
            for (size_t i = 0; i < elements_number; ++i)
            {
                ValueType v;
                if (!objects[i].get_property("v", v))
                {
                    LOG_ERROR_TAG("Serialization", "Failed to get property from ArchiveObject {}", name);
                    return false;
                }
                out.push_back(static_cast<ValueType>(v));
            }
        }
        return true;
    }

    [[nodiscard]] virtual reflection::Property& get_property_from_map(PropertyName name);
    [[nodiscard]] virtual const reflection::Property& get_property_from_map(PropertyName name) const;
    virtual reflection::Property& add_property_to_map(PropertyName name, reflection::Property&& property);

#ifdef PORTAL_DEBUG
    std::unordered_map<std::string, reflection::Property> property_map;
#else
    llvm::StringMap<reflection::Property> property_map;
#endif

    friend class JsonArchive;
};

template <>
inline void ArchiveObject::add_property<bool>(const PropertyName& name, const bool& b)
{
    add_property_to_map(name, {Buffer::create<bool>(b), reflection::PropertyType::boolean, reflection::PropertyContainerType::scalar, 1});
}

template <>
inline void ArchiveObject::add_property<uint128_t>(const PropertyName& name, const uint128_t& t)
{
    add_property_to_map(name, {Buffer::create<uint128_t>(t), reflection::PropertyType::integer128, reflection::PropertyContainerType::scalar, 1});
}

template <>
inline bool ArchiveObject::get_property<bool>(const PropertyName& name, bool& out)
{
    const auto& property = get_property_from_map(name);
    if (property.type == reflection::PropertyType::invalid)
        return false;

    out = *property.value.as<bool*>();
    return true;
}

template <>
inline bool ArchiveObject::get_property<uint128_t>(const PropertyName& name, uint128_t& out)
{
    const auto& property = get_property_from_map(name);
    if (property.type == reflection::PropertyType::invalid)
        return false;

    out = *property.value.as<uint128_t*>();
    return true;
}
} // namespace portal