Add example for custom (de)serialization
This commit is contained in:
parent
daf1a8602c
commit
76a8f649bc
|
@ -144,6 +144,15 @@ So beside the `BOOST_HANA_DEFINE_STRUCT` macro, the usage remains the same.
|
|||
Checkout the test cases for further examples. Relevant files are in
|
||||
the directories `lib/tests` and `generator/tests`.
|
||||
|
||||
### Custom (de)serialization
|
||||
Sometimes it is appropriate to implement custom (de)serialization. For instance, a
|
||||
custom object representing a time value should likey be serialized as a string rather
|
||||
than an object with the internal data members.
|
||||
|
||||
An example for such custom (de)serialization can be found in the file
|
||||
`json/reflector-chronoutilities.h`. It provides (de)serialization of `DateTime` and
|
||||
`TimeSpan` objects from the C++ utilities library.
|
||||
|
||||
## Install instructions
|
||||
|
||||
### Dependencies
|
||||
|
|
2
TODOs.md
2
TODOs.md
|
@ -12,4 +12,4 @@
|
|||
- [ ] Support `std::unique_ptr` and `std::shared_ptr`
|
||||
- [ ] Support `std::map` and `std::unordered_map`
|
||||
- [ ] Support `std::any`
|
||||
- [ ] Support/document customized (de)serialization (eg. serialize some `DateTime` object to ISO string representation)
|
||||
- [X] Support/document customized (de)serialization (eg. serialize some `DateTime` object to ISO string representation)
|
||||
|
|
|
@ -7,6 +7,7 @@ set(META_PROJECT_TYPE library)
|
|||
set(HEADER_FILES
|
||||
json/reflector.h
|
||||
json/reflector-boosthana.h
|
||||
json/reflector-chronoutilities.h
|
||||
json/serializable.h
|
||||
json/errorhandling.h
|
||||
)
|
||||
|
@ -18,6 +19,7 @@ set(TEST_SRC_FILES
|
|||
tests/cppunit.cpp
|
||||
tests/jsonreflector.cpp
|
||||
tests/jsonreflector-boosthana.cpp
|
||||
tests/jsonreflector-chronoutilities.cpp
|
||||
)
|
||||
set(CMAKE_MODULE_FILES
|
||||
cmake/modules/ReflectionGenerator.cmake
|
||||
|
|
|
@ -22,8 +22,9 @@ namespace ReflectiveRapidJSON {
|
|||
* \brief The JsonDeserializationErrorKind enum specifies which kind of error happend when populating variables from parsing results.
|
||||
*/
|
||||
enum class JsonDeserializationErrorKind : byte {
|
||||
TypeMismatch,
|
||||
ArraySizeMismatch,
|
||||
TypeMismatch, /**< The expected type does not match the type actually present in the JSON document. */
|
||||
ArraySizeMismatch, /**< The expected array size does not match the actual size of the JSON array. A fixed size is expected when deserializing an std::tuple. */
|
||||
ConversionError, /**< The expected type matches the type present in the JSON document, but further conversion of the value failed. */
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -39,6 +40,8 @@ enum class JsonType : byte {
|
|||
Object,
|
||||
};
|
||||
|
||||
// define helper functions which return the JsonType for the C++ type specified as template parameter
|
||||
|
||||
template <typename Type,
|
||||
Traits::EnableIf<Traits::Not<std::is_same<Type, bool>>, Traits::Any<std::is_integral<Type>, std::is_floating_point<Type>>>...>
|
||||
constexpr JsonType jsonType()
|
||||
|
@ -75,8 +78,6 @@ constexpr JsonType jsonType()
|
|||
constexpr JsonType jsonType(RAPIDJSON_NAMESPACE::Type type)
|
||||
{
|
||||
switch (type) {
|
||||
case RAPIDJSON_NAMESPACE::kNullType:
|
||||
return JsonType::Null;
|
||||
case RAPIDJSON_NAMESPACE::kFalseType:
|
||||
case RAPIDJSON_NAMESPACE::kTrueType:
|
||||
return JsonType::Bool;
|
||||
|
@ -146,6 +147,7 @@ struct JsonDeserializationErrors : public std::vector<JsonDeserializationError>
|
|||
|
||||
template <typename ExpectedType> void reportTypeMismatch(RAPIDJSON_NAMESPACE::Type presentType);
|
||||
void reportArraySizeMismatch();
|
||||
void reportConversionError(JsonType jsonType);
|
||||
|
||||
/// \brief The name of the class or struct which is currently being processed.
|
||||
const char *currentRecord;
|
||||
|
@ -154,7 +156,10 @@ struct JsonDeserializationErrors : public std::vector<JsonDeserializationError>
|
|||
/// \brief The index in the array which is currently processed.
|
||||
std::size_t currentIndex;
|
||||
/// \brief The list of fatal error types in form of flags.
|
||||
enum class ThrowOn : unsigned char { None = 0, TypeMismatch = 0x1, ArraySizeMismatch = 0x2 } throwOn;
|
||||
enum class ThrowOn : byte { None = 0, TypeMismatch = 0x1, ArraySizeMismatch = 0x2, ConversionError = 0x4 } throwOn;
|
||||
|
||||
private:
|
||||
void throwMaybe(ThrowOn on) const;
|
||||
};
|
||||
|
||||
/*!
|
||||
|
@ -173,7 +178,19 @@ inline JsonDeserializationErrors::JsonDeserializationErrors()
|
|||
*/
|
||||
constexpr JsonDeserializationErrors::ThrowOn operator|(JsonDeserializationErrors::ThrowOn lhs, JsonDeserializationErrors::ThrowOn rhs)
|
||||
{
|
||||
return static_cast<JsonDeserializationErrors::ThrowOn>(static_cast<unsigned char>(lhs) | static_cast<unsigned char>(rhs));
|
||||
return static_cast<JsonDeserializationErrors::ThrowOn>(static_cast<byte>(lhs) | static_cast<byte>(rhs));
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Throws the last error if its type is considered critical.
|
||||
* \param on Specifies the type of the last error as ThrowOn mask.
|
||||
* \remarks Behaviour is undefined if no error is present.
|
||||
*/
|
||||
inline void JsonDeserializationErrors::throwMaybe(ThrowOn on) const
|
||||
{
|
||||
if (static_cast<byte>(throwOn) & static_cast<byte>(on)) {
|
||||
throw back();
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -183,9 +200,7 @@ template <typename ExpectedType> inline void JsonDeserializationErrors::reportTy
|
|||
{
|
||||
emplace_back(
|
||||
JsonDeserializationErrorKind::TypeMismatch, jsonType<ExpectedType>(), jsonType(presentType), currentRecord, currentMember, currentIndex);
|
||||
if (static_cast<unsigned char>(throwOn) & static_cast<unsigned char>(ThrowOn::TypeMismatch)) {
|
||||
throw back();
|
||||
}
|
||||
throwMaybe(ThrowOn::TypeMismatch);
|
||||
}
|
||||
|
||||
/*!
|
||||
|
@ -196,9 +211,18 @@ template <typename ExpectedType> inline void JsonDeserializationErrors::reportTy
|
|||
inline void JsonDeserializationErrors::reportArraySizeMismatch()
|
||||
{
|
||||
emplace_back(JsonDeserializationErrorKind::ArraySizeMismatch, JsonType::Array, JsonType::Array, currentRecord, currentMember, currentIndex);
|
||||
if (static_cast<unsigned char>(throwOn) & static_cast<unsigned char>(ThrowOn::ArraySizeMismatch)) {
|
||||
throw back();
|
||||
}
|
||||
throwMaybe(ThrowOn::ArraySizeMismatch);
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Reports a conversion error. An error of that kind occurs when the JSON type matched the expected type, but further conversion of the value has failed.
|
||||
* \todo Allow specifying the error message.
|
||||
* \remarks This can happen when doing custom mapping (eg. when interpreting a JSON string as time value).
|
||||
*/
|
||||
inline void JsonDeserializationErrors::reportConversionError(JsonType jsonType)
|
||||
{
|
||||
emplace_back(JsonDeserializationErrorKind::ConversionError, jsonType, jsonType, currentRecord, currentMember, currentIndex);
|
||||
throwMaybe(ThrowOn::ConversionError);
|
||||
}
|
||||
|
||||
} // namespace ReflectiveRapidJSON
|
||||
|
|
|
@ -0,0 +1,121 @@
|
|||
#ifndef REFLECTIVE_RAPIDJSON_JSON_REFLECTOR_CHRONO_UTILITIES_H
|
||||
#define REFLECTIVE_RAPIDJSON_JSON_REFLECTOR_CHRONO_UTILITIES_H
|
||||
|
||||
/*!
|
||||
* \file reflector-chronoutilities.h
|
||||
* \brief Contains functions for (de)serializing objects from the chrono utilities provided by the
|
||||
* C++ utilities library.
|
||||
* \remarks This file demonstrates implementing custom (de)serialization for specific types.
|
||||
*/
|
||||
|
||||
#include "./reflector.h"
|
||||
|
||||
#include <c++utilities/chrono/datetime.h>
|
||||
#include <c++utilities/chrono/timespan.h>
|
||||
#include <c++utilities/conversion/conversionexception.h>
|
||||
|
||||
namespace ReflectiveRapidJSON {
|
||||
namespace JsonReflector {
|
||||
|
||||
// define functions to "push" values to a RapidJSON array or object
|
||||
|
||||
template <>
|
||||
inline void push<ChronoUtilities::DateTime>(
|
||||
const ChronoUtilities::DateTime &reflectable, RAPIDJSON_NAMESPACE::Value::Array &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)
|
||||
{
|
||||
const std::string str(reflectable.toIsoString());
|
||||
value.PushBack(RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>>(str.data(), str.size(), allocator), allocator);
|
||||
}
|
||||
|
||||
template <>
|
||||
inline void push<ChronoUtilities::DateTime>(const ChronoUtilities::DateTime &reflectable, const char *name, RAPIDJSON_NAMESPACE::Value::Object &value,
|
||||
RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)
|
||||
{
|
||||
const std::string str(reflectable.toIsoString());
|
||||
value.AddMember(RAPIDJSON_NAMESPACE::StringRef(name),
|
||||
RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>>(str.data(), str.size(), allocator), allocator);
|
||||
}
|
||||
|
||||
template <>
|
||||
inline void push<ChronoUtilities::TimeSpan>(
|
||||
const ChronoUtilities::TimeSpan &reflectable, RAPIDJSON_NAMESPACE::Value::Array &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)
|
||||
{
|
||||
const std::string str(reflectable.toString());
|
||||
value.PushBack(RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>>(str.data(), str.size(), allocator), allocator);
|
||||
}
|
||||
|
||||
template <>
|
||||
inline void push<ChronoUtilities::TimeSpan>(const ChronoUtilities::TimeSpan &reflectable, const char *name, RAPIDJSON_NAMESPACE::Value::Object &value,
|
||||
RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)
|
||||
{
|
||||
const std::string str(reflectable.toString());
|
||||
value.AddMember(RAPIDJSON_NAMESPACE::StringRef(name),
|
||||
RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>>(str.data(), str.size(), allocator), allocator);
|
||||
}
|
||||
|
||||
// define functions to "pull" values from a RapidJSON array or object
|
||||
|
||||
template <>
|
||||
inline void pull<ChronoUtilities::DateTime>(ChronoUtilities::DateTime &reflectable,
|
||||
RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>>::ValueIterator &value, JsonDeserializationErrors *errors)
|
||||
{
|
||||
std::string asString;
|
||||
pull(asString, value, errors);
|
||||
try {
|
||||
reflectable = ChronoUtilities::DateTime::fromIsoStringGmt(asString.data());
|
||||
} catch (const ConversionUtilities::ConversionException &) {
|
||||
if (errors) {
|
||||
errors->reportConversionError(JsonType::String);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
inline void pull<ChronoUtilities::DateTime>(ChronoUtilities::DateTime &reflectable,
|
||||
const RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>> &value, JsonDeserializationErrors *errors)
|
||||
{
|
||||
std::string asString;
|
||||
pull(asString, value, errors);
|
||||
try {
|
||||
reflectable = ChronoUtilities::DateTime::fromIsoStringGmt(asString.data());
|
||||
} catch (const ConversionUtilities::ConversionException &) {
|
||||
if (errors) {
|
||||
errors->reportConversionError(JsonType::String);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
inline void pull<ChronoUtilities::TimeSpan>(ChronoUtilities::TimeSpan &reflectable,
|
||||
RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>>::ValueIterator &value, JsonDeserializationErrors *errors)
|
||||
{
|
||||
std::string asString;
|
||||
pull(asString, value, errors);
|
||||
try {
|
||||
reflectable = ChronoUtilities::TimeSpan::fromString(asString.data());
|
||||
} catch (const ConversionUtilities::ConversionException &) {
|
||||
if (errors) {
|
||||
errors->reportConversionError(JsonType::String);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
template <>
|
||||
inline void pull<ChronoUtilities::TimeSpan>(ChronoUtilities::TimeSpan &reflectable,
|
||||
const RAPIDJSON_NAMESPACE::GenericValue<RAPIDJSON_NAMESPACE::UTF8<char>> &value, JsonDeserializationErrors *errors)
|
||||
{
|
||||
std::string asString;
|
||||
pull(asString, value, errors);
|
||||
try {
|
||||
reflectable = ChronoUtilities::TimeSpan::fromString(asString.data());
|
||||
} catch (const ConversionUtilities::ConversionException &) {
|
||||
if (errors) {
|
||||
errors->reportConversionError(JsonType::String);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace JsonReflector
|
||||
} // namespace ReflectiveRapidJSON
|
||||
|
||||
#endif // REFLECTIVE_RAPIDJSON_JSON_REFLECTOR_CHRONO_UTILITIES_H
|
|
@ -0,0 +1,117 @@
|
|||
#include "../json/reflector-chronoutilities.h"
|
||||
#include "../json/serializable.h"
|
||||
|
||||
#include <c++utilities/tests/testutils.h>
|
||||
|
||||
#include <cppunit/TestFixture.h>
|
||||
#include <cppunit/extensions/HelperMacros.h>
|
||||
|
||||
#include <rapidjson/document.h>
|
||||
#include <rapidjson/stringbuffer.h>
|
||||
#include <rapidjson/writer.h>
|
||||
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
using namespace std;
|
||||
using namespace CPPUNIT_NS;
|
||||
using namespace RAPIDJSON_NAMESPACE;
|
||||
using namespace ChronoUtilities;
|
||||
using namespace TestUtilities::Literals;
|
||||
using namespace ReflectiveRapidJSON;
|
||||
|
||||
/*!
|
||||
* \brief The JsonReflectorChronoUtilitiesTests class tests the custom (de)serialization of the chrono utilities provided
|
||||
* by the C++ utilities library.
|
||||
* \remarks In these tests, the (de)serialization is customized so the code generator is not involved.
|
||||
*/
|
||||
class JsonReflectorChronoUtilitiesTests : public TestFixture {
|
||||
CPPUNIT_TEST_SUITE(JsonReflectorChronoUtilitiesTests);
|
||||
CPPUNIT_TEST(testSerializeing);
|
||||
CPPUNIT_TEST(testDeserializeing);
|
||||
CPPUNIT_TEST(testErroHandling);
|
||||
CPPUNIT_TEST_SUITE_END();
|
||||
|
||||
public:
|
||||
JsonReflectorChronoUtilitiesTests();
|
||||
|
||||
void testSerializeing();
|
||||
void testDeserializeing();
|
||||
void testErroHandling();
|
||||
|
||||
private:
|
||||
const DateTime m_dateTime;
|
||||
const TimeSpan m_timeSpan;
|
||||
const string m_string;
|
||||
};
|
||||
|
||||
CPPUNIT_TEST_SUITE_REGISTRATION(JsonReflectorChronoUtilitiesTests);
|
||||
|
||||
JsonReflectorChronoUtilitiesTests::JsonReflectorChronoUtilitiesTests()
|
||||
: m_dateTime(DateTime::fromDateAndTime(2017, 4, 2, 15, 31, 21, 165.125))
|
||||
, m_timeSpan(TimeSpan::fromHours(3.25) + TimeSpan::fromSeconds(19.125))
|
||||
, m_string("[\"2017-04-02T15:31:21.165125\",\"03:15:19.125\"]")
|
||||
{
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests serializing DateTime and TimeSpan objects.
|
||||
*/
|
||||
void JsonReflectorChronoUtilitiesTests::testSerializeing()
|
||||
{
|
||||
Document doc(kArrayType);
|
||||
Document::AllocatorType &alloc = doc.GetAllocator();
|
||||
doc.SetArray();
|
||||
Document::Array array(doc.GetArray());
|
||||
|
||||
JsonReflector::push(m_dateTime, array, alloc);
|
||||
JsonReflector::push(m_timeSpan, array, alloc);
|
||||
|
||||
StringBuffer strbuf;
|
||||
Writer<StringBuffer> jsonWriter(strbuf);
|
||||
doc.Accept(jsonWriter);
|
||||
CPPUNIT_ASSERT_EQUAL(m_string, string(strbuf.GetString()));
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests deserializing DateTime and TimeSpan objects.
|
||||
*/
|
||||
void JsonReflectorChronoUtilitiesTests::testDeserializeing()
|
||||
{
|
||||
Document doc(kArrayType);
|
||||
|
||||
doc.Parse(m_string.data(), m_string.size());
|
||||
auto array = doc.GetArray().begin();
|
||||
|
||||
DateTime dateTime;
|
||||
TimeSpan timeSpan;
|
||||
JsonDeserializationErrors errors;
|
||||
JsonReflector::pull(dateTime, array, &errors);
|
||||
JsonReflector::pull(timeSpan, array, &errors);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(0_st, errors.size());
|
||||
CPPUNIT_ASSERT_EQUAL(m_dateTime.toIsoString(), dateTime.toIsoString());
|
||||
CPPUNIT_ASSERT_EQUAL(m_timeSpan.toString(), timeSpan.toString());
|
||||
}
|
||||
|
||||
/*!
|
||||
* \brief Tests deserializing DateTime and TimeSpan objects (error case).
|
||||
*/
|
||||
void JsonReflectorChronoUtilitiesTests::testErroHandling()
|
||||
{
|
||||
Document doc(kArrayType);
|
||||
|
||||
doc.Parse("[\"2017-04-31T15:31:21.165125\",\"03:15:19.125\"]");
|
||||
auto array = doc.GetArray().begin();
|
||||
|
||||
DateTime dateTime;
|
||||
TimeSpan timeSpan;
|
||||
JsonDeserializationErrors errors;
|
||||
JsonReflector::pull(dateTime, array, &errors);
|
||||
JsonReflector::pull(timeSpan, array, &errors);
|
||||
|
||||
CPPUNIT_ASSERT_EQUAL(1_st, errors.size());
|
||||
CPPUNIT_ASSERT(dateTime.isNull());
|
||||
CPPUNIT_ASSERT_EQUAL(m_timeSpan.toString(), timeSpan.toString());
|
||||
}
|
Loading…
Reference in New Issue