From 762540f5e5a62c35b1f9feaf60e1fbe269f756d9 Mon Sep 17 00:00:00 2001 From: Martchus Date: Sat, 7 May 2022 18:40:37 +0200 Subject: [PATCH] Add support for `std::optional` --- README.md | 5 ++-- TODOs.md | 2 +- lib/binary/reflector.h | 28 ++++++++++++++++------ lib/json/reflector.h | 27 ++++++++++++++++++--- lib/tests/binaryreflector.cpp | 27 +++++++++++++++++++++ lib/tests/jsonreflector.cpp | 45 +++++++++++++++++++++++++++++++++++ 6 files changed, 121 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 3a281f7..288dc21 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,7 @@ The following table shows the mapping of supported C++ types to supported JSON t | iteratable lists (`std::vector`, `std::list`, ...) | array | | sets (`std::set`, `std::unordered_set`, `std::multiset`, ...) | array | | `std::pair`, `std::tuple` | array | -| `std::unique_ptr`, `std::shared_ptr` | depends/null | +| `std::unique_ptr`, `std::shared_ptr`, `std::optional` | depends/null | | `std::map`, `std::unordered_map`, `std::multimap`, `std::unordered_multimap` | object | | `std::variant` | object | | `JsonSerializable` | object | @@ -73,7 +73,8 @@ The following table shows the mapping of supported C++ types to supported JSON t * For the same reason `const char *` and `std::string_view` are only supported for serialization. * Enums are (de)serialized as their underlying integer value. When deserializing, it is currently *not* checked whether the present integer value is a valid enumeration item. -* The JSON type for smart pointers depends on the type the pointer refers to. It can also be `null`. +* The JSON type for smart pointers and `std::optional` depends on the type the pointer/optional refers to. + It can also be `null` for null pointers or `std::optional` without value. * If multiple `std::shared_ptr` instances point to the same object this object is serialized multiple times. When deserializing those identical objects, it is currently not possible to share the memory (again). So each `std::shared_ptr` will point to its own copy. Note that this limitation is *not* present when using binary diff --git a/TODOs.md b/TODOs.md index ff29a86..0eeb80d 100644 --- a/TODOs.md +++ b/TODOs.md @@ -29,5 +29,5 @@ - [x] Support `std::unique_ptr` and `std::shared_ptr` - [x] Support `std::map` and `std::unordered_map` - [ ] Support `std::any` -- [ ] Support `std::optional` +- [x] Support `std::optional` - [x] Support/document customized (de)serialization (eg. serialize some `DateTime` object to ISO string representation) diff --git a/lib/binary/reflector.h b/lib/binary/reflector.h index 5e5e6a5..c789542 100644 --- a/lib/binary/reflector.h +++ b/lib/binary/reflector.h @@ -17,6 +17,7 @@ #include #include #include +#include #include #include @@ -46,7 +47,8 @@ namespace BinaryReflector { template using IsBuiltInType = Traits::Any, - Traits::IsIteratable, Traits::IsSpecializingAnyOf, std::is_enum, IsVariant>; + Traits::IsIteratable, Traits::IsSpecializingAnyOf, std::is_enum, + IsVariant>; template using IsCustomType = Traits::Not>; class BinaryDeserializer; @@ -78,6 +80,7 @@ public: template > * = nullptr> void read(Type &pair); template > * = nullptr> void read(Type &pointer); template > * = nullptr> void read(Type &pointer); + template > * = nullptr> void read(Type &pointer); template , Traits::IsResizable> * = nullptr> void read(Type &iteratable); template , IsMultiMapOrHash> * = nullptr> void read(Type &iteratable); template > * = nullptr> void write(const Type &pair); - template > * = nullptr> void write(const Type &pointer); + template > * = nullptr> + void write(const Type &pointer); template > * = nullptr> void write(const Type &pointer); template , Traits::HasSize> * = nullptr> void write(const Type &iteratable); template > * = nullptr> void write(const Type &enumValue); @@ -159,6 +163,16 @@ template > *> void BinaryDeserializer::read(Type &opt) +{ + if (!readBool()) { + opt.reset(); + return; + } + opt = std::make_optional(); + read(*opt); +} + template , Traits::IsResizable> *> void BinaryDeserializer::read(Type &iteratable) { const auto size = readVariableLengthUIntBE(); @@ -247,12 +261,12 @@ template > *> void BinarySerializer::write(const Type &pointer) +template > *> +void BinarySerializer::write(const Type &opt) { - const bool hasValue = pointer != nullptr; - writeBool(hasValue); - if (hasValue) { - write(*pointer); + writeBool(static_cast(opt)); + if (opt) { + write(*opt); } } diff --git a/lib/json/reflector.h b/lib/json/reflector.h index 0f34369..e2c89c6 100644 --- a/lib/json/reflector.h +++ b/lib/json/reflector.h @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -83,7 +84,7 @@ inline RAPIDJSON_NAMESPACE::Document parseJsonDocFromString(const char *json, st template using IsBuiltInType = Traits::Any, std::is_floating_point, std::is_pointer, std::is_enum, Traits::IsSpecializingAnyOf, Traits::IsIteratable, - Traits::IsSpecializingAnyOf, IsVariant>; + Traits::IsSpecializingAnyOf, IsVariant>; template using IsCustomType = Traits::Not>; // define trait to check for custom structs/classes which are JSON serializable @@ -306,10 +307,10 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_ } /*! - * \brief Pushes the specified unique_ptr, shared_ptr or weak_ptr to the specified value. + * \brief Pushes the specified unique_ptr, shared_ptr, weak_ptr or optional to the specified value. */ template > * = nullptr> + Traits::EnableIfAny> * = nullptr> void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) { if (!reflectable) { @@ -478,6 +479,12 @@ void pull(Type &reflectable, const rapidjson::GenericValue> * = nullptr> void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors); +/*! + * \brief Pulls the specified \a reflectable which is an std::optional from the specified value which might be null. + */ +template > * = nullptr> +void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors); + /*! * \brief Pulls the specified \a reflectable which is a variant from the specified value which might be null. */ @@ -848,6 +855,20 @@ void pull(Type &reflectable, const rapidjson::GenericValue> *> +void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors) +{ + if (value.IsNull()) { + reflectable.reset(); + return; + } + reflectable = std::make_optional(); + pull(*reflectable, value, errors); +} + /// \cond namespace Detail { template diff --git a/lib/tests/binaryreflector.cpp b/lib/tests/binaryreflector.cpp index 50ee263..34f38e9 100644 --- a/lib/tests/binaryreflector.cpp +++ b/lib/tests/binaryreflector.cpp @@ -170,6 +170,7 @@ class BinaryReflectorTests : public TestFixture { CPPUNIT_TEST(testSmallSharedPointer); CPPUNIT_TEST(testBigSharedPointer); CPPUNIT_TEST(testVariant); + CPPUNIT_TEST(testOptional); CPPUNIT_TEST_SUITE_END(); public: @@ -187,6 +188,7 @@ public: void testSmallSharedPointer(); void testBigSharedPointer(); void testVariant(); + void testOptional(); private: vector m_buffer; @@ -391,3 +393,28 @@ void BinaryReflectorTests::testVariant() CPPUNIT_ASSERT_EQUAL("foo"s, get<0>(deserializedVariants.anotherVariant)); CPPUNIT_ASSERT_EQUAL(42, get<1>(deserializedVariants.yetAnotherVariant)); } + +void BinaryReflectorTests::testOptional() +{ + // create test objects + const auto str = std::make_optional("foo"); + const auto nullStr = std::optional(); + + // serialize test object + auto stream = std::stringstream(std::ios_base::in | std::ios_base::out | std::ios_base::binary); + stream.exceptions(std::ios_base::failbit | std::ios_base::badbit); + auto ser = BinaryReflector::BinarySerializer(&stream); + ser.write(str); + ser.write(nullStr); + + // deserialize the object again + auto deser = BinaryReflector::BinaryDeserializer(&stream); + auto deserStr = std::optional(); + auto deserNullStr = std::optional(); + deser.read(deserStr); + deser.read(deserNullStr); + + CPPUNIT_ASSERT(deserStr.has_value()); + CPPUNIT_ASSERT_EQUAL("foo"s, deserStr.value()); + CPPUNIT_ASSERT(!nullStr.has_value()); +} diff --git a/lib/tests/jsonreflector.cpp b/lib/tests/jsonreflector.cpp index a9204e4..bccbe43 100644 --- a/lib/tests/jsonreflector.cpp +++ b/lib/tests/jsonreflector.cpp @@ -186,11 +186,13 @@ class JsonReflectorTests : public TestFixture { CPPUNIT_TEST(testSerializeNestedObjects); CPPUNIT_TEST(testSerializeUniquePtr); CPPUNIT_TEST(testSerializeSharedPtr); + CPPUNIT_TEST(testSerializeOptional); CPPUNIT_TEST(testDeserializePrimitives); CPPUNIT_TEST(testDeserializeSimpleObjects); CPPUNIT_TEST(testDeserializeNestedObjects); CPPUNIT_TEST(testDeserializeUniquePtr); CPPUNIT_TEST(testDeserializeSharedPtr); + CPPUNIT_TEST(testDeserializeOptional); CPPUNIT_TEST(testHandlingParseError); CPPUNIT_TEST(testHandlingTypeMismatch); CPPUNIT_TEST_SUITE_END(); @@ -205,11 +207,13 @@ public: void testSerializeNestedObjects(); void testSerializeUniquePtr(); void testSerializeSharedPtr(); + void testSerializeOptional(); void testDeserializePrimitives(); void testDeserializeSimpleObjects(); void testDeserializeNestedObjects(); void testDeserializeUniquePtr(); void testDeserializeSharedPtr(); + void testDeserializeOptional(); void testHandlingParseError(); void testHandlingTypeMismatch(); @@ -379,6 +383,28 @@ void JsonReflectorTests::testSerializeSharedPtr() string(strbuf.GetString())); } +/*! + * \brief Tests serializing std::optional. + */ +void JsonReflectorTests::testSerializeOptional() +{ + Document doc(kArrayType); + Document::AllocatorType &alloc = doc.GetAllocator(); + doc.SetArray(); + Document::Array array(doc.GetArray()); + + const auto str = make_optional("foo"); + const auto nullStr = std::optional(); + + JsonReflector::push(str, array, alloc); + JsonReflector::push(nullStr, array, alloc); + + StringBuffer strbuf; + Writer jsonWriter(strbuf); + doc.Accept(jsonWriter); + CPPUNIT_ASSERT_EQUAL("[\"foo\",null]"s, std::string(strbuf.GetString())); +} + /*! * \brief Tests deserializing strings, numbers (int, float, double) and boolean. */ @@ -517,6 +543,9 @@ void JsonReflectorTests::testDeserializeNestedObjects() CPPUNIT_ASSERT_EQUAL("test"s, nestedInVector[0].text); } +/*! + * \brief Tests deserializing std::optional. + */ void JsonReflectorTests::testDeserializeUniquePtr() { Document doc(kArrayType); @@ -561,6 +590,22 @@ void JsonReflectorTests::testDeserializeSharedPtr() CPPUNIT_ASSERT_EQUAL("bar"s, obj->text); } +void JsonReflectorTests::testDeserializeOptional() +{ + Document doc(kArrayType); + doc.Parse("[\"foo\",null]"); + auto array = doc.GetArray().begin(); + + optional str = "foo"s; + optional nullStr; + JsonDeserializationErrors errors; + JsonReflector::pull(str, array, &errors); + CPPUNIT_ASSERT_EQUAL(0_st, errors.size()); + CPPUNIT_ASSERT(str.has_value()); + CPPUNIT_ASSERT_EQUAL("foo"s, *str); + CPPUNIT_ASSERT(!nullStr.has_value()); +} + /*! * \brief Tests whether RAPIDJSON_NAMESPACE::ParseResult is thrown correctly when passing invalid JSON to fromJSON(). */