diff --git a/src/ecstasy/serialization/JsonSerializer.hpp b/src/ecstasy/serialization/JsonSerializer.hpp index 5c0a7cdcc..1028a043c 100644 --- a/src/ecstasy/serialization/JsonSerializer.hpp +++ b/src/ecstasy/serialization/JsonSerializer.hpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include "Serializer.hpp" @@ -36,6 +37,16 @@ namespace ecstasy::serialization /// class JsonSerializer : public Serializer { public: + /// + /// @brief Nested context operations, used to open and close nested objects and arrays in streams easily. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + enum class NestedContextOp { NewObject, NewArray, Close }; + /// @brief Alias for NestedContextOp + using OP = NestedContextOp; + /// /// @brief Construct a new Raw Serializer instance. /// @@ -131,12 +142,19 @@ namespace ecstasy::serialization std::is_bounded_array_v || // Bounded array util::meta::is_std_vector::value || // std::vector std::is_fundamental_v || // Fundamental type (int, float, etc.) - std::is_same_v // std::type_info + std::is_same_v || // std::type_info + std::is_same_v // Nested context operation , int>::type > // clang-format on JsonSerializer &saveImpl(const T &object) { - if constexpr (std::is_same_v || std::is_same_v + if constexpr (std::is_same_v) { + switch (object) { + case NestedContextOp::NewObject: newNestedObject(); break; + case NestedContextOp::NewArray: newNestedArray(); break; + case NestedContextOp::Close: closeNested(); break; + } + } else if constexpr (std::is_same_v || std::is_same_v || util::meta::is_type_bounded_array_v) { rapidjson::Value value; @@ -149,23 +167,23 @@ namespace ecstasy::serialization else value.SetString(object, _document.GetAllocator()); - _document.PushBack(value.Move(), _document.GetAllocator()); + addValue(std::move(value.Move())); } else if constexpr (std::is_bounded_array_v) { rapidjson::Value array(rapidjson::kArrayType); for (size_t i = 0; i < std::extent_v; i++) array.PushBack(object[i], _document.GetAllocator()); - _document.PushBack(array, _document.GetAllocator()); + addValue(std::move(array.Move())); } else if constexpr (util::meta::is_std_vector::value) { rapidjson::Value array(rapidjson::kArrayType); for (const auto &elem : object) array.PushBack(elem, _document.GetAllocator()); - _document.PushBack(array, _document.GetAllocator()); + addValue(std::move(array.Move())); } else if constexpr (std::is_same_v) { save(object.hash_code()); } else if constexpr (std::is_fundamental_v) { - _document.PushBack(object, _document.GetAllocator()); + addValue(rapidjson::Value(object)); } else { return Parent::save(object); } @@ -178,8 +196,129 @@ namespace ecstasy::serialization return 0; // loadRaw(); } + /// + /// @brief Open a new nested object or array context in the current object (see @ref getWriteCursor). + /// The context can be closed with @ref closeNested. + /// + /// @warning If the context is an object you must set a key before adding a value to it. + /// + /// @param[in] type Type of the nested object or array. + /// + /// @return JsonSerializer& Reference to @b this for chain calls. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + JsonSerializer &newNested(rapidjson::Type type) + { + if (type != rapidjson::Type::kObjectType && type != rapidjson::Type::kArrayType) + throw std::invalid_argument("Invalid type for nested object."); + + rapidjson::Value &cursor = getWriteCursor(); + std::string key = _nextKey; + + addValue(rapidjson::Value(type)); + if (cursor.IsArray()) + _stack.push(cursor[cursor.Size() - 1]); + else + _stack.push(cursor[key.c_str()]); + return *this; + } + + /// + /// @brief Wrapper for @ref newNested(rapidjson::Type::kObjectType). + /// + /// @return JsonSerializer& Reference to @b this for chain calls. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + JsonSerializer &newNestedObject() + { + return newNested(rapidjson::Type::kObjectType); + } + + /// + /// @brief Wrapper for @ref newNested(rapidjson::Type::kArrayType). + /// + /// @return JsonSerializer& Reference to @b this for chain calls. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + JsonSerializer &newNestedArray() + { + return newNested(rapidjson::Type::kArrayType); + } + + /// + /// @brief Close the current nested object or array opened with @ref newNestedObject or @ref newNestedArray + /// (or @ref newNested). + /// + /// @return JsonSerializer& Reference to @b this for chain calls. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + JsonSerializer &closeNested() + { + _stack.pop(); + return *this; + } + + /// + /// @brief Get a reference to the current cursor (ie nested objects or arrays). + /// + /// @return rapidjson::Value& Reference to the current cursor. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + rapidjson::Value &getWriteCursor() + { + if (_stack.empty()) + return _document; + return _stack.top(); + } + + /// + /// @brief Add a value to the current object or array. + /// If the current object is an array, the value will be appended to it. + /// If the current object is an object, the first call will set the key (string) for the next value, and the + /// second call will add the value to the object (using the previous key identifier). + /// + /// @param[in] value Value to add. + /// + /// @return JsonSerializer& Reference to @b this for chain calls. + /// + /// @author Andréas Leroux (andreas.leroux@epitech.eu) + /// @since 1.0.0 (2024-10-10) + /// + JsonSerializer &addValue(rapidjson::Value &&value) + { + rapidjson::Value &cursor = getWriteCursor(); + + if (cursor.IsObject()) { + if (_nextKey.empty()) { + if (value.IsString()) + _nextKey = value.GetString(); + else + throw std::logic_error("Json object key is missing."); + } else { + cursor.AddMember( + rapidjson::Value(_nextKey.c_str(), _document.GetAllocator()), value, _document.GetAllocator()); + _nextKey.clear(); + } + } else { + cursor.PushBack(value, _document.GetAllocator()); + } + return *this; + } + private: rapidjson::Document _document; + std::stack> _stack; + std::string _nextKey; }; } // namespace ecstasy::serialization diff --git a/tests/serialization/tests_Serializer.cpp b/tests/serialization/tests_Serializer.cpp index 7c2d1b927..d488996ea 100644 --- a/tests/serialization/tests_Serializer.cpp +++ b/tests/serialization/tests_Serializer.cpp @@ -29,6 +29,11 @@ struct Position { return serializer.appendRaw(*this); } + JsonSerializer &operator>>(JsonSerializer &serializer) const + { + return serializer << JsonSerializer::OP::NewObject << "x" << x << "y" << y << JsonSerializer::OP::Close; + } + Position &operator<<(RawSerializer &serializer) { serializer >> x >> y; @@ -56,6 +61,12 @@ struct NPC { return serializer << pos << std::string_view(name); } + JsonSerializer &operator>>(JsonSerializer &serializer) const + { + return serializer << JsonSerializer::OP::NewObject << "pos" << pos << "name" << std::string_view(name) + << JsonSerializer::OP::Close; + } + NPC &operator<<(RawSerializer &serializer) { serializer >> pos >> name; @@ -359,4 +370,13 @@ TEST(JsonSerializer, all) jsonSerializer << someInts << vec; json = jsonSerializer.exportBytes(); GTEST_ASSERT_EQ(json, "[[1,2,3,4,5,6,7,8,9,10],[1,2,3,4,5]]"); + + jsonSerializer.clear(); + // Compound types + Position pos{1.0f, -8456.0f}; + NPC npc{Position(42.f, 0.f), "Steve"}; + + jsonSerializer << pos << npc; + json = jsonSerializer.exportBytes(); + GTEST_ASSERT_EQ(json, "[{\"x\":1.0,\"y\":-8456.0},{\"pos\":{\"x\":42.0,\"y\":0.0},\"name\":\"Steve\"}]"); }