-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(serialization): add JSONDeserializer
- Loading branch information
Showing
6 changed files
with
435 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.