Add example for custom (de)serialization

This commit is contained in:
Martchus 2017-11-03 17:45:16 +01:00
parent daf1a8602c
commit 76a8f649bc
6 changed files with 286 additions and 13 deletions

View File

@ -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

View File

@ -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)

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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());
}