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(ArchiveObject&& other) noexcept;
    ArchiveObject& operator=(ArchiveObject&& other) noexcept;

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

    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;
        Buffer buffer = Buffer::allocate(t.size() * sizeof(ArchiveObject));

        constexpr auto property_type = (reflection::String<ValueT>)
                                           ? reflection::PropertyType::null_term_string
                                           : (ArchiveableConcept<ValueT> || ExternalArchiveableConcept<ValueT> || std::same_as<ValueT, ArchiveObject>)
                                           ? reflection::PropertyType::object
                                           : reflection::get_property_type<ValueT>();

        if constexpr (ArchiveableConcept<ValueT> || ExternalArchiveableConcept<ValueT>)
        {
            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);
            }
        }
        else if constexpr (std::same_as<ValueT, ArchiveObject>)
        {
            for (size_t i = 0; i < t.size(); ++i)
            {
                new(buffer.as<ArchiveObject*>() + i) ArchiveObject(std::move(const_cast<ValueT&>(t[i])));
            }
        }
        else
        {
            for (size_t i = 0; i < t.size(); ++i)
            {
                auto* object = new(buffer.as<ArchiveObject*>() + i) ArchiveObject();
                object->add_property("v", t[i]);
            }
        }

        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::IsVec T>
    void add_property(const PropertyName& name, const T& t)
    {
        constexpr auto element_number = T::length();

        add_property_to_map(
            name,
            {Buffer::create<T>(t), reflection::get_property_type<typename T::value_type>(), reflection::PropertyContainerType::vector, element_number}
        );
    }

    template <reflection::IsMatrix T>
    void add_property(const PropertyName& name, const T& t)
    {
        constexpr auto element_number = T::length() * T::col_type::length();

        add_property_to_map(
            name,
            {Buffer::create<T>(t), reflection::get_property_type<typename T::value_type>(), reflection::PropertyContainerType::matrix, element_number}
        );
    }

    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>
    void add_property(const PropertyName& name, const std::optional<T>& optional_t)
    {
        if (optional_t.has_value())
        {
            add_property(name, optional_t.value());
        }
    }

    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::IsVec T>
    bool get_property(const PropertyName& name, T& out)
    {
        [[maybe_unused]] constexpr auto element_number = T::length();

        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;

        // Native storage (from add_property<IsVec>)
        if (property.container_type == reflection::PropertyContainerType::vector)
        {
            PORTAL_ASSERT(property.elements_number == element_number, "Property {} elements number mismatch", name);
            out = T(*property.value.as<T*>());
            return true;
        }

        // Array storage (from JSON deserialization)
        if (property.container_type == reflection::PropertyContainerType::array)
        {
            return format_vec<T>(name, property, out);
        }

        LOG_ERROR_TAG("Serialization", "Property {} container type mismatch (expected vector or array)", name);
        return false;
    }

    template <reflection::IsMatrix T>
    bool get_property(const PropertyName& name, T& out)
    {
        constexpr auto element_number = T::length() * T::col_type::length();

        const auto& property = get_property_from_map(name);
        if (property.type == reflection::PropertyType::invalid)
            return false;

        // Native storage (from add_property<IsMatrix>)
        if (property.container_type == reflection::PropertyContainerType::matrix)
        {
            PORTAL_ASSERT(property.elements_number == element_number, "Property {} elements number mismatch", name);
            out = T(*property.value.as<T*>());
            return true;
        }

        // Array storage (from JSON deserialization)
        if (property.container_type == reflection::PropertyContainerType::array)
        {
            return format_mat<T>(name, property, out);
        }

        LOG_ERROR_TAG("Serialization", "Property {} container type mismatch (expected matrix or array)", name);
        return false;
    }

    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;
        }
    }

    template <typename T>
    void get_property(const PropertyName& name, std::optional<T>& optional_t)
    {
        T value;
        const bool found = get_property(name, value);
        if (found)
            optional_t = std::optional<T>{std::move(value)};
        else
            optional_t = std::nullopt;
    }

    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 if constexpr (std::same_as<ValueType, ArchiveObject>)
        {
            for (size_t i = 0; i < elements_number; ++i)
            {
                auto* objects = value.as<ArchiveObject*>();
                out.emplace_back(std::move(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;
    }

    template <reflection::IsVec T>
    bool format_vec(const PropertyName& name, const reflection::Property& prop, T& out) const
    {
        constexpr auto element_number = T::length();
        const auto& [value, type, container_type, elements_number] = prop;

        if (elements_number != element_number)
        {
            LOG_ERROR_TAG("Serialization", "Property {} array size {} does not match vec size {}", name, elements_number, element_number);
            return false;
        }

        auto* objects = value.as<ArchiveObject*>();
        for (size_t i = 0; i < element_number; ++i)
        {
            typename T::value_type v;
            if (!objects[i].get_property("v", v))
            {
                LOG_ERROR_TAG("Serialization", "Failed to get element {} from array for vec property {}", i, name);
                return false;
            }
            out[static_cast<typename T::length_type>(i)] = v;
        }
        return true;
    }

    template <reflection::IsMatrix T>
    bool format_mat(const PropertyName& name, const reflection::Property& prop, T& out) const
    {
        constexpr auto cols = T::length();
        constexpr auto rows = T::col_type::length();
        constexpr auto element_number = cols * rows;
        const auto& [value, type, container_type, elements_number] = prop;

        if (elements_number != element_number)
        {
            LOG_ERROR_TAG("Serialization", "Property {} array size {} does not match matrix size {}", name, elements_number, element_number);
            return false;
        }

        auto* objects = value.as<ArchiveObject*>();
        for (typename T::length_type col = 0; col < cols; ++col)
        {
            for (typename T::length_type row = 0; row < rows; ++row)
            {
                typename T::value_type v;
                if (!objects[static_cast<size_t>(col * rows + row)].get_property("v", v))
                {
                    LOG_ERROR_TAG("Serialization", "Failed to get element [{},{}] from array for matrix property {}", col, row, name);
                    return false;
                }
                out[col][row] = 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