Skip to content

Commit

Permalink
Implement deserialization of JsonSerializer
Browse files Browse the repository at this point in the history
Linked: #164
  • Loading branch information
AndreasLrx committed Oct 13, 2024
1 parent 4a11ad9 commit be7e08a
Show file tree
Hide file tree
Showing 2 changed files with 261 additions and 18 deletions.
214 changes: 199 additions & 15 deletions src/ecstasy/serialization/JsonSerializer.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,22 @@ namespace ecstasy::serialization
///
/// @brief Nested context operations, used to open and close nested objects and arrays in streams easily.
///
/// @note Use the static constexpr instances to use them in stream operations.
///
/// @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;
enum class NestedContextOp {
NewObject, ///< Open a new object (Creating it in save mode)
NewArray, ///< Open a new array (Creating it in save mode)
Close ///< Close the current object or array
};
/// @copydoc NestedContextOp::NewObject
static constexpr NestedContextOp NewObject = NestedContextOp::NewObject;
/// @copydoc NestedContextOp::NewArray
static constexpr NestedContextOp NewArray = NestedContextOp::NewArray;
/// @copydoc NestedContextOp::Close
static constexpr NestedContextOp Close = NestedContextOp::Close;

///
/// @brief Construct a new Raw Serializer instance.
Expand Down Expand Up @@ -135,6 +145,21 @@ namespace ecstasy::serialization
_document.Accept(writer);
}

///
/// @brief Reset the json read/write cursor to the begining of the document.
///
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-10-11)
///
void resetCursor()
{
while (!_stack.empty())
_stack.pop();
while (!_arrayIterators.empty())
_arrayIterators.pop();
}

/// @copydoc save
// clang-format off
template <typename T,
typename = typename std::enable_if<
Expand Down Expand Up @@ -190,6 +215,155 @@ namespace ecstasy::serialization
return *this;
}

/// @copydoc update
template <typename U>
requires std::is_same_v<U, NestedContextOp>
JsonSerializer &updateImpl(const U &op)
{
switch (op) {
case NestedContextOp::NewObject: newNestedObject(false); break;
case NestedContextOp::NewArray: newNestedArray(false); break;
case NestedContextOp::Close: closeNested(); break;
}
return *this;
}

/// @copydoc update
// clang-format off
template <typename U,
typename = typename std::enable_if<(
std::is_same_v<U, std::string> || std::is_same_v<U, std::string_view> || // Load
std::is_bounded_array_v<U> || // Bounded array
util::meta::is_std_vector<U>::value) // std::vector
, int>::type>
// clang-format on
JsonSerializer &updateImpl(U &object)
{
// Handle string key for object values
if constexpr (std::is_same_v<U, std::string> || std::is_same_v<U, std::string_view>
|| util::meta::is_type_bounded_array_v<U, char>) {
if (getWriteCursor().IsObject() && _nextKey.empty())
_nextKey = object;
else {
if constexpr (std::is_same_v<U, std::string>)
object = load<U>();
else
throw std::invalid_argument("string_view and char[] can only be used for object keys.");
}
}
// Handle bounded arrays
else if constexpr (std::is_bounded_array_v<U>) {
newNestedArray(false);

// Need to fetch the new cursor after the new nested array
if (getWriteCursor().Size() != std::extent_v<U>)
throw std::invalid_argument("Array size mismatch.");

for (size_t i = 0; i < std::extent_v<U>; i++)
update(object[i]);
closeNested();
}
// Handle std::vector
else if constexpr (util::meta::is_std_vector<U>::value) {
newNestedArray(false);
rapidjson::SizeType size = getWriteCursor().Size();

object.clear();
object.reserve(size);
for (size_t i = 0; i < size; i++) {
if constexpr (is_constructible<typename U::value_type>)
object.emplace_back(*this);
else
object.push_back(std::move(load<typename U::value_type>()));
}
closeNested();
} else
return Parent::update(object);
return *this;
}

/// @copydoc load
// clang-format off
template <typename U>
requires std::is_fundamental_v<U> || // Fundamental type
std::is_same_v<U, std::string>
// clang-format on
U loadImpl()
{
const rapidjson::Value &value = readCurrentValue(true);

// Read Boolean
if constexpr (std::is_same_v<U, bool>) {
return value.GetBool();
}
// Read Integer
else if constexpr (std::numeric_limits<U>::is_integer) {
if constexpr (std::numeric_limits<U>::is_signed) {
if constexpr (sizeof(U) < 8)
return static_cast<U>(value.GetInt());
else
return static_cast<U>(value.GetInt64());
} else {
if constexpr (sizeof(U) < 8)
return static_cast<U>(value.GetUint());
else
return static_cast<U>(value.GetUint64());
}
}
// Read Floating point
else if constexpr (std::is_floating_point_v<U>) {
if constexpr (sizeof(U) == 4)
return value.GetFloat();
else
return value.GetDouble();
}
// Read String
else if constexpr (std::is_same_v<U, std::string>) {
return std::string(value.GetString(), value.GetStringLength());
}
}

///
/// @brief Access the current value in the json document, and move the cursor to the next value.
///
/// @param[in] andMoveCursor If true, move the cursor to the next value.
///
/// @return rapidjson::Value& Reference to the current value.
///
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-10-11)
///
rapidjson::Value &readCurrentValue(bool andMoveCursor = true)
{
rapidjson::Value &cursor = getWriteCursor();

if (cursor.IsObject()) {
if (_nextKey.empty())
throw std::invalid_argument("No key set for object value.");
rapidjson::Value &result = cursor[_nextKey.c_str()];
if (andMoveCursor)
_nextKey.clear();
return result;
} else {
if (!cursor.IsArray()) {
throw std::invalid_argument("Invalid cursor type. Expected object or array.");
}
// If the array is empty, push the first element (first document element)
if (_arrayIterators.empty())
_arrayIterators.push(cursor.Begin());

// If we reached the end of the array, throw an exception
if (_arrayIterators.top() == cursor.End())
throw std::out_of_range("End of array reached.");

rapidjson::Value *value = _arrayIterators.top();
// Move the cursor to the next element
if (andMoveCursor)
++_arrayIterators.top();
return *value;
}
}

/// @copydoc loadComponentHash
std::size_t loadComponentHash() override final
{
Expand All @@ -203,25 +377,32 @@ namespace ecstasy::serialization
/// @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.
/// @param[in] create If true, create the nested object or array (for save mode).
///
/// @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)
JsonSerializer &newNested(rapidjson::Type type, bool create = true)
{
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;
if (create) {
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()]);
addValue(rapidjson::Value(type));
if (cursor.IsArray())
_stack.push(cursor[cursor.Size() - 1]);
else
_stack.push(cursor[key.c_str()]);
} else {
_stack.push(readCurrentValue());
if (getWriteCursor().IsArray())
_arrayIterators.push(getWriteCursor().Begin());
}
return *this;
}

Expand All @@ -233,9 +414,9 @@ namespace ecstasy::serialization
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-10-10)
///
JsonSerializer &newNestedObject()
JsonSerializer &newNestedObject(bool create = true)
{
return newNested(rapidjson::Type::kObjectType);
return newNested(rapidjson::Type::kObjectType, create);
}

///
Expand All @@ -246,9 +427,9 @@ namespace ecstasy::serialization
/// @author Andréas Leroux ([email protected])
/// @since 1.0.0 (2024-10-10)
///
JsonSerializer &newNestedArray()
JsonSerializer &newNestedArray(bool create = true)
{
return newNested(rapidjson::Type::kArrayType);
return newNested(rapidjson::Type::kArrayType, create);
}

///
Expand All @@ -262,6 +443,8 @@ namespace ecstasy::serialization
///
JsonSerializer &closeNested()
{
if (_stack.top().get().IsArray())
_arrayIterators.pop();
_stack.pop();
return *this;
}
Expand Down Expand Up @@ -318,6 +501,7 @@ namespace ecstasy::serialization
private:
rapidjson::Document _document;
std::stack<std::reference_wrapper<rapidjson::Value>> _stack;
std::stack<rapidjson::Value::ValueIterator> _arrayIterators;
std::string _nextKey;
};
} // namespace ecstasy::serialization
Expand Down
65 changes: 62 additions & 3 deletions tests/serialization/tests_Serializer.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,13 @@ struct Position {

JsonSerializer &operator>>(JsonSerializer &serializer) const
{
return serializer << JsonSerializer::OP::NewObject << "x" << x << "y" << y << JsonSerializer::OP::Close;
return serializer << JsonSerializer::NewObject << "x" << x << "y" << y << JsonSerializer::Close;
}

Position &operator<<(JsonSerializer &serializer)
{
serializer >> JsonSerializer::NewObject >> "x" >> x >> "y" >> y >> JsonSerializer::Close;
return *this;
}

Position &operator<<(RawSerializer &serializer)
Expand Down Expand Up @@ -63,8 +69,14 @@ struct NPC {

JsonSerializer &operator>>(JsonSerializer &serializer) const
{
return serializer << JsonSerializer::OP::NewObject << "pos" << pos << "name" << std::string_view(name)
<< JsonSerializer::OP::Close;
return serializer << JsonSerializer::NewObject << "pos" << pos << "name" << std::string_view(name)
<< JsonSerializer::Close;
}

NPC &operator<<(JsonSerializer &serializer)
{
serializer >> JsonSerializer::NewObject >> "pos" >> pos >> "name" >> name >> JsonSerializer::Close;
return *this;
}

NPC &operator<<(RawSerializer &serializer)
Expand Down Expand Up @@ -360,6 +372,23 @@ TEST(JsonSerializer, all)
std::string json = jsonSerializer.exportBytes();
GTEST_ASSERT_EQ(json, "[21,42,84,168,-45612.0,\"\",\"this is a test\",\"raw string\"]");

jsonSerializer.resetCursor();
uint8_t u8 = 0;
uint16_t u16 = 0;
uint32_t u32 = 0;
uint64_t u64 = 0;
float f = 0;
std::string emptyLoaded, loaded, rawLoaded;
jsonSerializer >> u8 >> u16 >> u32 >> u64 >> f >> emptyLoaded >> loaded >> rawLoaded;
GTEST_ASSERT_EQ(u8, 21);
GTEST_ASSERT_EQ(u16, 42);
GTEST_ASSERT_EQ(u32, 84);
GTEST_ASSERT_EQ(u64, 168);
GTEST_ASSERT_EQ(f, -45612.f);
GTEST_ASSERT_EQ(emptyLoaded, "");
GTEST_ASSERT_EQ(loaded, "this is a test");
GTEST_ASSERT_EQ(rawLoaded, "raw string");

jsonSerializer.clear();
// Array types
std::vector<int> vec(5);
Expand All @@ -371,6 +400,17 @@ TEST(JsonSerializer, all)
json = jsonSerializer.exportBytes();
GTEST_ASSERT_EQ(json, "[[1,2,3,4,5,6,7,8,9,10],[1,2,3,4,5]]");

jsonSerializer.resetCursor();
int updated[std::extent_v<decltype(someInts)>];
std::vector<int> updatedVec;
jsonSerializer >> updated >> updatedVec;
for (int i = 0; i < 10; i++) {
GTEST_ASSERT_EQ(updated[i], i + 1);
if (i < 5) {
GTEST_ASSERT_EQ(updatedVec[i], i + 1);
}
}

jsonSerializer.clear();
// Compound types
Position pos{1.0f, -8456.0f};
Expand All @@ -379,4 +419,23 @@ TEST(JsonSerializer, all)
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\"}]");

jsonSerializer.resetCursor();
Position posUpdated;
NPC npcUpdated;
jsonSerializer >> posUpdated >> npcUpdated;
GTEST_ASSERT_EQ(posUpdated.x, 1.0f);
GTEST_ASSERT_EQ(posUpdated.y, -8456.0f);
GTEST_ASSERT_EQ(npcUpdated.name, "Steve");
GTEST_ASSERT_EQ(npcUpdated.pos.x, 42.f);
GTEST_ASSERT_EQ(npcUpdated.pos.y, 0.f);

jsonSerializer.resetCursor();
Position posLoaded = jsonSerializer.load<Position>();
NPC npcLoaded = jsonSerializer.load<NPC>();
GTEST_ASSERT_EQ(posLoaded.x, 1.0f);
GTEST_ASSERT_EQ(posLoaded.y, -8456.0f);
GTEST_ASSERT_EQ(npcLoaded.name, "Steve");
GTEST_ASSERT_EQ(npcLoaded.pos.x, 42.f);
GTEST_ASSERT_EQ(npcLoaded.pos.y, 0.f);
}

0 comments on commit be7e08a

Please sign in to comment.