From 76a8f649bcea018f6db14b68fdfa179d886a742f Mon Sep 17 00:00:00 2001 From: Martchus Date: Fri, 3 Nov 2017 17:45:16 +0100 Subject: [PATCH] Add example for custom (de)serialization --- README.md | 9 ++ TODOs.md | 2 +- lib/CMakeLists.txt | 2 + lib/json/errorhandling.h | 48 ++++++-- lib/json/reflector-chronoutilities.h | 121 ++++++++++++++++++++ lib/tests/jsonreflector-chronoutilities.cpp | 117 +++++++++++++++++++ 6 files changed, 286 insertions(+), 13 deletions(-) create mode 100644 lib/json/reflector-chronoutilities.h create mode 100644 lib/tests/jsonreflector-chronoutilities.cpp diff --git a/README.md b/README.md index 8ac8a9e..ced06d3 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/TODOs.md b/TODOs.md index b9f4e8a..689ac3a 100644 --- a/TODOs.md +++ b/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) \ No newline at end of file +- [X] Support/document customized (de)serialization (eg. serialize some `DateTime` object to ISO string representation) diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 2e79921..c844002 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -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 diff --git a/lib/json/errorhandling.h b/lib/json/errorhandling.h index 46e1e92..304acd8 100644 --- a/lib/json/errorhandling.h +++ b/lib/json/errorhandling.h @@ -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 >, Traits::Any, std::is_floating_point>>...> 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 template 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 /// \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(static_cast(lhs) | static_cast(rhs)); + return static_cast(static_cast(lhs) | static_cast(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(throwOn) & static_cast(on)) { + throw back(); + } } /*! @@ -183,9 +200,7 @@ template inline void JsonDeserializationErrors::reportTy { emplace_back( JsonDeserializationErrorKind::TypeMismatch, jsonType(), jsonType(presentType), currentRecord, currentMember, currentIndex); - if (static_cast(throwOn) & static_cast(ThrowOn::TypeMismatch)) { - throw back(); - } + throwMaybe(ThrowOn::TypeMismatch); } /*! @@ -196,9 +211,18 @@ template inline void JsonDeserializationErrors::reportTy inline void JsonDeserializationErrors::reportArraySizeMismatch() { emplace_back(JsonDeserializationErrorKind::ArraySizeMismatch, JsonType::Array, JsonType::Array, currentRecord, currentMember, currentIndex); - if (static_cast(throwOn) & static_cast(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 diff --git a/lib/json/reflector-chronoutilities.h b/lib/json/reflector-chronoutilities.h new file mode 100644 index 0000000..d52d72c --- /dev/null +++ b/lib/json/reflector-chronoutilities.h @@ -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 +#include +#include + +namespace ReflectiveRapidJSON { +namespace JsonReflector { + +// define functions to "push" values to a RapidJSON array or object + +template <> +inline void push( + 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>(str.data(), str.size(), allocator), allocator); +} + +template <> +inline void push(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>(str.data(), str.size(), allocator), allocator); +} + +template <> +inline void push( + 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>(str.data(), str.size(), allocator), allocator); +} + +template <> +inline void push(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>(str.data(), str.size(), allocator), allocator); +} + +// define functions to "pull" values from a RapidJSON array or object + +template <> +inline void pull(ChronoUtilities::DateTime &reflectable, + RAPIDJSON_NAMESPACE::GenericValue>::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 &reflectable, + const RAPIDJSON_NAMESPACE::GenericValue> &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 &reflectable, + RAPIDJSON_NAMESPACE::GenericValue>::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 &reflectable, + const RAPIDJSON_NAMESPACE::GenericValue> &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 diff --git a/lib/tests/jsonreflector-chronoutilities.cpp b/lib/tests/jsonreflector-chronoutilities.cpp new file mode 100644 index 0000000..4d07ef0 --- /dev/null +++ b/lib/tests/jsonreflector-chronoutilities.cpp @@ -0,0 +1,117 @@ +#include "../json/reflector-chronoutilities.h" +#include "../json/serializable.h" + +#include + +#include +#include + +#include +#include +#include + +#include +#include +#include + +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 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()); +}