Skip to content

Commit

Permalink
Implement nested object/array serialization in JsonSerializer
Browse files Browse the repository at this point in the history
Linked: #164
  • Loading branch information
AndreasLrx committed Oct 13, 2024
1 parent ae8d8c7 commit 4a11ad9
Show file tree
Hide file tree
Showing 2 changed files with 165 additions and 6 deletions.
151 changes: 145 additions & 6 deletions src/ecstasy/serialization/JsonSerializer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <rapidjson/pointer.h>
#include <rapidjson/writer.h>
#include <sstream>
#include <stack>
#include <vector>

#include "Serializer.hpp"
Expand All @@ -36,6 +37,16 @@ namespace ecstasy::serialization
///
class JsonSerializer : public Serializer<JsonSerializer> {
public:
///
/// @brief Nested context operations, used to open and close nested objects and arrays in streams easily.
///
/// @author Andréas Leroux ([email protected])
/// @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.
///
Expand Down Expand Up @@ -131,12 +142,19 @@ namespace ecstasy::serialization
std::is_bounded_array_v<T> || // Bounded array
util::meta::is_std_vector<T>::value || // std::vector
std::is_fundamental_v<T> || // Fundamental type (int, float, etc.)
std::is_same_v<T, std::type_info> // std::type_info
std::is_same_v<T, std::type_info> || // std::type_info
std::is_same_v<T, NestedContextOp> // Nested context operation
, int>::type >
// clang-format on
JsonSerializer &saveImpl(const T &object)
{
if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>
if constexpr (std::is_same_v<T, NestedContextOp>) {
switch (object) {
case NestedContextOp::NewObject: newNestedObject(); break;
case NestedContextOp::NewArray: newNestedArray(); break;
case NestedContextOp::Close: closeNested(); break;
}
} else if constexpr (std::is_same_v<T, std::string> || std::is_same_v<T, std::string_view>
|| util::meta::is_type_bounded_array_v<T, char>) {
rapidjson::Value value;

Expand All @@ -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<T>) {
rapidjson::Value array(rapidjson::kArrayType);

for (size_t i = 0; i < std::extent_v<T>; 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<T>::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<T, std::type_info>) {
save(object.hash_code());
} else if constexpr (std::is_fundamental_v<T>) {
_document.PushBack(object, _document.GetAllocator());
addValue(rapidjson::Value(object));
} else {
return Parent::save(object);
}
Expand All @@ -178,8 +196,129 @@ namespace ecstasy::serialization
return 0; // loadRaw<std::size_t>();
}

///
/// @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 ([email protected])
/// @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 ([email protected])
/// @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 ([email protected])
/// @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 ([email protected])
/// @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 ([email protected])
/// @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 ([email protected])
/// @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<std::reference_wrapper<rapidjson::Value>> _stack;
std::string _nextKey;
};
} // namespace ecstasy::serialization

Expand Down
20 changes: 20 additions & 0 deletions tests/serialization/tests_Serializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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\"}]");
}

0 comments on commit 4a11ad9

Please sign in to comment.