Skip to content

Commit

Permalink
feat(serialization): add JSONDeserializer
Browse files Browse the repository at this point in the history
  • Loading branch information
RiscadoA committed Nov 12, 2023
1 parent 1b5eb92 commit a17d287
Show file tree
Hide file tree
Showing 6 changed files with 435 additions and 1 deletion.
1 change: 1 addition & 0 deletions core/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ set(CUBOS_CORE_SOURCE
"src/cubos/core/data/ser/serializer.cpp"
"src/cubos/core/data/ser/debug.cpp"
"src/cubos/core/data/des/deserializer.cpp"
"src/cubos/core/data/des/json.cpp"

"src/cubos/core/io/window.cpp"
"src/cubos/core/io/cursor.cpp"
Expand Down
36 changes: 36 additions & 0 deletions core/include/cubos/core/data/des/json.hpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/// @file
/// @brief Class @ref cubos::core::data::JSONDeserializer.
/// @ingroup core-data-des

#pragma once

#include <nlohmann/json.hpp>

#include <cubos/core/data/des/deserializer.hpp>

namespace cubos::core::data
{
/// @brief Deserializer implementation which allows reading data from a JSON object.
///
/// Before deserializing any data, a JSON object must be fed to the deserializer.
///
/// @ingroup core-data-des
class JSONDeserializer : public Deserializer
{
public:
/// @brief Constructs.
JSONDeserializer();

/// @brief Feeds a JSON object to be deserialized.
/// @param json JSON object.
void feed(nlohmann::json json);

protected:
bool decompose(const reflection::Type& type, void* value) override;

private:
nlohmann::json mJSON;
nlohmann::json::iterator mIterator;
bool mIsKey;
};
} // namespace cubos::core::data
250 changes: 250 additions & 0 deletions core/src/cubos/core/data/des/json.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,250 @@
#include <cubos/core/data/des/json.hpp>
#include <cubos/core/log.hpp>
#include <cubos/core/memory/any_value.hpp>
#include <cubos/core/reflection/external/primitives.hpp>
#include <cubos/core/reflection/external/string.hpp>
#include <cubos/core/reflection/traits/array.hpp>
#include <cubos/core/reflection/traits/constructible.hpp>
#include <cubos/core/reflection/traits/dictionary.hpp>
#include <cubos/core/reflection/traits/fields.hpp>
#include <cubos/core/reflection/traits/string_conversion.hpp>
#include <cubos/core/reflection/type.hpp>

using cubos::core::data::JSONDeserializer;
using cubos::core::memory::AnyValue;
using cubos::core::reflection::ArrayTrait;
using cubos::core::reflection::ConstructibleTrait;
using cubos::core::reflection::DictionaryTrait;
using cubos::core::reflection::FieldsTrait;
using cubos::core::reflection::StringConversionTrait;
using cubos::core::reflection::Type;

// NOLINTBEGIN(bugprone-macro-parentheses)
#define AUTO_HOOK(T, fromString) \
this->hook<T>([this](T& value) { \
if (mIsKey) \
{ \
try \
{ \
value = static_cast<T>(fromString(mIterator.key())); \
} \
catch (std::invalid_argument&) \
{ \
CUBOS_WARN("Cannot deserialize '{}' from JSON key {}: invalid string", #T, mIterator.key()); \
return false; \
} \
catch (std::out_of_range&) \
{ \
CUBOS_WARN("Cannot deserialize '{}' from JSON key {}: out of range", #T, mIterator.key()); \
return false; \
} \
} \
else \
{ \
try \
{ \
value = mIterator.value(); \
} \
catch (nlohmann::json::exception&) \
{ \
CUBOS_WARN("Cannot deserialize '{}' from a JSON {}", #T, mIterator->type_name()); \
return false; \
} \
} \
return true; \
})
// NOLINTEND(bugprone-macro-parentheses)

JSONDeserializer::JSONDeserializer()
{
AUTO_HOOK(bool, [](const std::string& s) {
if (s == "true")
{
return true;
}

if (s == "false")
{
return false;
}

throw std::invalid_argument{s};
});
AUTO_HOOK(int8_t, std::stol);
AUTO_HOOK(int16_t, std::stol);
AUTO_HOOK(int32_t, std::stol);
AUTO_HOOK(int64_t, std::stoll);
AUTO_HOOK(uint8_t, std::stoul);
AUTO_HOOK(uint16_t, std::stoul);
AUTO_HOOK(uint32_t, std::stoul);
AUTO_HOOK(uint64_t, std::stoull);
AUTO_HOOK(float, std::stoull);
AUTO_HOOK(double, std::stoull);
AUTO_HOOK(std::string, [](const std::string& s) { return s; });
}

void JSONDeserializer::feed(nlohmann::json json)
{
mJSON = nlohmann::json::array({std::move(json)});
mIterator = mJSON.begin();
mIsKey = false;
}

bool JSONDeserializer::decompose(const Type& type, void* value)
{
if (mIsKey)
{
if (!type.has<StringConversionTrait>())
{
// JSON doesn't support structured keys, as JSON keys are just strings
CUBOS_WARN("Type '{}' cannot be deserialized from a dictionary key - try defining a hook", type.name());
return false;
}

const auto& trait = type.get<StringConversionTrait>();
if (!trait.from(value, mIterator.key()))
{
CUBOS_WARN("Key '{}' is not a valid string representation of '{}'", mIterator.key(), type.name());
return false;
}

return true;
}

if (mIterator->is_string() && type.has<StringConversionTrait>())
{
const auto& trait = type.get<StringConversionTrait>();
if (!trait.from(value, mIterator.value()))
{
CUBOS_WARN("String '{}' is not a valid string representation of '{}'", mIterator.value(), type.name());
return false;
}

return true;
}

if (mIterator->is_object() && type.has<DictionaryTrait>())
{
const auto& trait = type.get<DictionaryTrait>();
auto view = trait.view(value);

if (view.length() > 0)
{
if (!trait.hasErase())
{
CUBOS_WARN("Dictionary type '{}' must be empty to be deserialized as it doesn't support erase",
type.name());
return false;
}

view.clear();
}

if (!trait.keyType().has<ConstructibleTrait>())
{
CUBOS_WARN("Dictionary type '{}' cannot be deserialized because its key type '{}' isn't constructible",
type.name(), trait.keyType().name());
return false;
}

auto key = AnyValue::defaultConstruct(trait.keyType());
for (auto it = mIterator->begin(), end = mIterator->end(); it != end; ++it)
{
mIterator = it;
mIsKey = true;
if (!this->read(key.type(), key.get()))
{
CUBOS_WARN("Couldn't deserialize dictionary key '{}'", it.key());
return false;
}

view.insertDefault(key.get());
auto entry = view.find(key.get());
CUBOS_ASSERT(entry != view.end()); // We just inserted the key...

mIterator = it;
mIsKey = false;
if (!this->read(trait.valueType(), entry->value))
{
CUBOS_WARN("Couldn't deserialize dictionary value with key '{}'", it.key());
return false;
}
}

return true;
}

if (mIterator->is_array() && type.has<ArrayTrait>())
{
const auto& trait = type.get<ArrayTrait>();
auto view = trait.view(value);

if (mIterator->size() != view.length())
{
if (!trait.hasResize())
{
CUBOS_WARN(
"Array type '{}' isn't resizable and the JSON array size ({}) doesn't match the its size ({})",
type.name(), mIterator->size(), view.length());
return false;
}

view.resize(mIterator->size());
}

auto it = mIterator->begin();
for (std::size_t i = 0; i < view.length(); ++i, ++it)
{
mIterator = it;
mIsKey = false;
if (!this->read(trait.elementType(), view.get(i)))
{
CUBOS_WARN("Couldn't deserialize array element {}", i);
return false;
}
}

return true;
}

if (type.has<FieldsTrait>())
{
const auto& trait = type.get<FieldsTrait>();
auto view = trait.view(value);

if (trait.size() == 1)
{
// If there's a single field, read it directly.
if (!this->read(trait.begin()->type(), view.begin()->value))
{
CUBOS_WARN("Couldn't deserialize wrapped field '{}'", trait.begin()->name());
return false;
}

return true;
}

for (auto it = mIterator->begin(), end = mIterator->end(); it != end; ++it)
{
const auto* field = trait.field(it.key());
if (field == nullptr)
{
CUBOS_WARN("Type '{}' has no such field '{}'", type.name(), it.key());
return false;
}

mIterator = it;
mIsKey = false;
if (!this->read(field->type(), view.get(*field)))
{
CUBOS_WARN("Couldn't deserialize field '{}'", it.key());
return false;
}
}

return true;
}

CUBOS_WARN("Cannot deserialize type '{}' from a JSON {}", type.name(), mIterator->type_name());
return false;
}
2 changes: 1 addition & 1 deletion core/src/cubos/core/reflection/traits/fields.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ const FieldsTrait::Field* FieldsTrait::field(const std::string& name) const
}
}

CUBOS_ERROR("No such field '{}'", name);
CUBOS_DEBUG("No such field '{}'", name);
return nullptr;
}

Expand Down
1 change: 1 addition & 0 deletions core/tests/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ add_executable(
data/fs/standard_archive.cpp
data/fs/file_system.cpp
data/ser/debug.cpp
data/des/json.cpp
data/context.cpp

memory/any_value.cpp
Expand Down
Loading

0 comments on commit a17d287

Please sign in to comment.