From 03e3a4bc67dbfc637435f6c655c8de2e0ab81a3b Mon Sep 17 00:00:00 2001 From: Martchus Date: Mon, 26 Feb 2018 22:37:43 +0100 Subject: [PATCH] Support std::(unordered_)?(multi)?set --- CMakeLists.txt | 2 +- README.md | 31 ++++++------ lib/json/errorhandling.h | 15 +++++- lib/json/reflector.h | 96 +++++++++++++++++++++++++++++++++---- lib/tests/jsonreflector.cpp | 63 +++++++++++++++++++----- 5 files changed, 169 insertions(+), 38 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 75d164d..f104b38 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -10,7 +10,7 @@ set(META_APP_CATEGORIES "Utility;") set(META_GUI_OPTIONAL false) set(META_VERSION_MAJOR 0) set(META_VERSION_MINOR 0) -set(META_VERSION_PATCH 3) +set(META_VERSION_PATCH 4) set(META_APP_VERSION ${META_VERSION_MAJOR}.${META_VERSION_MINOR}.${META_VERSION_PATCH}) # set project name for IDEs like Qt Creator diff --git a/README.md b/README.md index 6ca2bfa..dfc2d1d 100644 --- a/README.md +++ b/README.md @@ -44,20 +44,21 @@ For a full list of further ideas, see [TODOs.md](./TODOs.md). ## Supported datatypes The following table shows the mapping of supported C++ types to supported JSON types: -| C++ type | JSON type | -| ------------------------------------------------- |:------------:| -| custom structures/classes | object | -| `bool` | true/false | -| signed and unsigned integral types | number | -| `float` and `double` | number | -| `enum` and `enum class` | number | -| `std::string` | string | -| `const char *` | string | -| iteratable lists (`std::vector`, `std::list`, ...)| array | -| `std::tuple` | array | -| `std::unique_ptr`, `std::shared_ptr` | depends/null | -| `std::map`, `std::unordered_map` | object | -| `JsonSerializable` | object | +| C++ type | JSON type | +| ------------------------------------------------------------- |:------------:| +| custom structures/classes | object | +| `bool` | true/false | +| signed and unsigned integral types | number | +| `float` and `double` | number | +| `enum` and `enum class` | number | +| `std::string` | string | +| `const char *` | string | +| iteratable lists (`std::vector`, `std::list`, ...) | array | +| sets (`std::set`, `std::unordered_set`, `std::multiset`, ...) | array | +| `std::tuple` | array | +| `std::unique_ptr`, `std::shared_ptr` | depends/null | +| `std::map`, `std::unordered_map` | object | +| `JsonSerializable` | object | ### Remarks * Raw pointer are not supported. This prevents @@ -313,7 +314,7 @@ An example for such custom (de)serialization can be found in the file The following diagram gives an overview about the architecture of the code generator and wrapper library around RapidJSON: -![Architectue overview](/doc/arch.svg) +![Architectue overview](./doc/arch.svg) * blue: classes from LibTooling/Clang * grey: conceivable extension or use diff --git a/lib/json/errorhandling.h b/lib/json/errorhandling.h index 7eaa1ef..e738e48 100644 --- a/lib/json/errorhandling.h +++ b/lib/json/errorhandling.h @@ -25,6 +25,7 @@ enum class JsonDeserializationErrorKind : byte { 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. */ + UnexpectedDuplicate, /**< The expected type matches the type present in the JSON document, but the value can not be added to the container because it is already present and duplicates are not allowed. */ }; /*! @@ -154,6 +155,7 @@ struct JsonDeserializationErrors : public std::vector template void reportTypeMismatch(RAPIDJSON_NAMESPACE::Type presentType); void reportArraySizeMismatch(); void reportConversionError(JsonType jsonType); + void reportUnexpectedDuplicate(JsonType jsonType); /// \brief The name of the class or struct which is currently being processed. const char *currentRecord; @@ -162,7 +164,7 @@ 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 : byte { None = 0, TypeMismatch = 0x1, ArraySizeMismatch = 0x2, ConversionError = 0x4 } throwOn; + enum class ThrowOn : byte { None = 0, TypeMismatch = 0x1, ArraySizeMismatch = 0x2, ConversionError = 0x4, UnexpectedDuplicate = 0x8 } throwOn; private: void throwMaybe(ThrowOn on) const; @@ -231,6 +233,17 @@ inline void JsonDeserializationErrors::reportConversionError(JsonType jsonType) throwMaybe(ThrowOn::ConversionError); } +/*! + * \brief Reports an unexpected duplicate. An error of that kind occurs when the JSON type matched the expected type, but the value can not be inserted in the container because it is already present and duplicates are not allowed. + * \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::reportUnexpectedDuplicate(JsonType jsonType) +{ + emplace_back(JsonDeserializationErrorKind::UnexpectedDuplicate, jsonType, jsonType, currentRecord, currentMember, currentIndex); + throwMaybe(ThrowOn::UnexpectedDuplicate); +} + } // namespace ReflectiveRapidJSON #endif // REFLECTIVE_RAPIDJSON_JSON_REFLECTOR_H diff --git a/lib/json/reflector.h b/lib/json/reflector.h index ef796d3..00e49b3 100644 --- a/lib/json/reflector.h +++ b/lib/json/reflector.h @@ -18,9 +18,11 @@ #include #include #include +#include #include #include #include +#include #include "./errorhandling.h" @@ -90,9 +92,15 @@ using IsJsonSerializable // define trait to check for map or hash template using IsMapOrHash = Traits::Any, Traits::IsSpecializationOf>; +template using IsSet = Traits::Any, Traits::IsSpecializationOf>; template -using IsArray +using IsMultiSet = Traits::Any, Traits::IsSpecializationOf>; +template +using IsArrayOrSet = Traits::All, Traits::Not>, Traits::Not>>; +template +using IsArray = Traits::All, Traits::Not>, + Traits::Not>, Traits::Not>, Traits::Not>>; // define functions to "push" values to a RapidJSON array or object @@ -194,7 +202,7 @@ inline void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAP /*! * \brief Pushes the specified iteratable (eg. std::vector, std::list) to the specified value. */ -template , Traits::HasSize>...> +template , Traits::HasSize>...> void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) { value.SetArray(); @@ -208,7 +216,7 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_ /*! * \brief Pushes the specified iteratable list (eg. std::vector, std::list) to the specified value. */ -template , Traits::Not>>...> +template , Traits::Not>>...> void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) { value.SetArray(); @@ -346,21 +354,33 @@ void pull(Type &reflectable, const RAPIDJSON_NAMESPACE::GenericValue, Traits::Not>>...> +template , Traits::Not>>...> void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors); /*! * \brief Pulls the specified \a reflectable which is an iteratable with reserve() method from the specified value which is checked to contain an array. */ -template , Traits::IsReservable>...> +template , Traits::IsReservable>...> void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors); /*! - * \brief Pulls the specified \a reflectable which is an iteratable from the specified array. The \a reflectable is cleared before. + * \brief Pulls the specified \a reflectable which is an array/vector/list from the specified array. The \a reflectable is cleared before. */ template >...> void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors); +/*! + * \brief Pulls the specified \a reflectable which is a set from the specified array. The \a reflectable is cleared before. + */ +template >...> +void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors); + +/*! + * \brief Pulls the specified \a reflectable which is a multiset from the specified array. The \a reflectable is cleared before. + */ +template >...> +void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors); + /*! * \brief Pulls the specified \a reflectable which is a map from the specified value which is checked to contain an object. */ @@ -492,7 +512,7 @@ inline void pull(Type &, const RAPIDJSON_NAMESPACE::GenericValue, Traits::Not>>...> +template , Traits::Not>>...> void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors) { if (!value.IsArray()) { @@ -507,7 +527,7 @@ void pull(Type &reflectable, const rapidjson::GenericValue, Traits::IsReservable>...> +template , Traits::IsReservable>...> void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors) { if (!value.IsArray()) { @@ -522,7 +542,7 @@ void pull(Type &reflectable, const rapidjson::GenericValue>...> void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors) @@ -537,9 +557,67 @@ void pull(Type &reflectable, rapidjson::GenericValuecurrentIndex = index; } + ++index; reflectable.emplace_back(); pull(reflectable.back(), item, errors); + } + + // clear error context + if (errors) { + errors->currentIndex = JsonDeserializationError::noIndex; + } +} + +/*! + * \brief Pulls the specified \a reflectable which is a multiset from the specified array. The \a reflectable is cleared before. + */ +template >...> +void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors) +{ + // clear previous contents of the array + reflectable.clear(); + + // pull all array elements of the specified value + std::size_t index = 0; + for (const rapidjson::GenericValue> &item : array) { + // set error context for current index + if (errors) { + errors->currentIndex = index; + } ++index; + typename Type::value_type itemObj; + pull(itemObj, item, errors); + reflectable.emplace(move(itemObj)); + } + + // clear error context + if (errors) { + errors->currentIndex = JsonDeserializationError::noIndex; + } +} + +/*! + * \brief Pulls the specified \a reflectable which is a set from the specified array. The \a reflectable is cleared before. + */ +template >...> +void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors) +{ + // clear previous contents of the array + reflectable.clear(); + + // pull all array elements of the specified value + std::size_t index = 0; + for (const rapidjson::GenericValue> &item : array) { + // set error context for current index + if (errors) { + errors->currentIndex = index; + } + ++index; + typename Type::value_type itemObj; + pull(itemObj, item, errors); + if (!reflectable.emplace(move(itemObj)).second) { + errors->reportUnexpectedDuplicate(JsonType::Array); + } } // clear error context diff --git a/lib/tests/jsonreflector.cpp b/lib/tests/jsonreflector.cpp index e975f15..c1fc510 100644 --- a/lib/tests/jsonreflector.cpp +++ b/lib/tests/jsonreflector.cpp @@ -33,7 +33,13 @@ using namespace ReflectiveRapidJSON; // test traits static_assert(JsonReflector::IsArray>::value, "vector mapped to array"); static_assert(JsonReflector::IsArray>::value, "list mapped to array"); -static_assert(!JsonReflector::IsArray::value, "string mapped to string"); +static_assert(!JsonReflector::IsArray>::value, "set not considered an array"); +static_assert(!JsonReflector::IsArray>::value, "multiset not considered an array"); +static_assert(JsonReflector::IsArrayOrSet>::value, "set is array or set"); +static_assert(JsonReflector::IsArrayOrSet>::value, "multiset is array or set"); +static_assert(JsonReflector::IsSet>::value, "set"); +static_assert(JsonReflector::IsMultiSet>::value, "multiset"); +static_assert(!JsonReflector::IsArray::value, "string not mapped to array though it is iteratable"); static_assert(JsonReflector::IsMapOrHash>::value, "map mapped to object"); static_assert(JsonReflector::IsMapOrHash>::value, "hash mapped to object"); static_assert(!JsonReflector::IsMapOrHash>::value, "vector not mapped to object"); @@ -49,6 +55,10 @@ struct TestObject : public JsonSerializable { bool boolean; map someMap; unordered_map someHash; + set someSet; + multiset someMultiset; + unordered_set someUnorderedSet; + unordered_multiset someUnorderedMultiset; }; struct NestingObject : public JsonSerializable { @@ -86,6 +96,10 @@ template <> inline void push(const TestObject &reflectable, Value::O push(reflectable.boolean, "boolean", value, allocator); push(reflectable.someMap, "someMap", value, allocator); push(reflectable.someHash, "someHash", value, allocator); + push(reflectable.someSet, "someSet", value, allocator); + push(reflectable.someMultiset, "someMultiset", value, allocator); + push(reflectable.someUnorderedSet, "someUnorderedSet", value, allocator); + push(reflectable.someUnorderedMultiset, "someUnorderedMultiset", value, allocator); } template <> inline void push(const NestingObject &reflectable, Value::Object &value, Document::AllocatorType &allocator) @@ -115,6 +129,10 @@ inline void pull(TestObject &reflectable, const GenericValuecurrentRecord = previousRecord; } @@ -258,8 +276,12 @@ void JsonReflectorTests::testSerializeSimpleObjects() testObj.boolean = false; testObj.someMap = { { "a", 1 }, { "b", 2 } }; testObj.someHash = { { "c", true }, { "d", false } }; + testObj.someSet = { "a", "b", "c" }; + testObj.someMultiset = { "a", "b", "b" }; + testObj.someUnorderedSet = { "a" }; + testObj.someUnorderedMultiset = { "b", "b", "b" }; CPPUNIT_ASSERT_EQUAL( - "{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{\"a\":1,\"b\":2},\"someHash\":{\"d\":false,\"c\":true}}"s, + "{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{\"a\":1,\"b\":2},\"someHash\":{\"d\":false,\"c\":true},\"someSet\":[\"a\",\"b\",\"c\"],\"someMultiset\":[\"a\",\"b\",\"b\"],\"someUnorderedSet\":[\"a\"],\"someUnorderedMultiset\":[\"b\",\"b\",\"b\"]}"s, string(testObj.toJson().GetString())); } @@ -277,7 +299,7 @@ void JsonReflectorTests::testSerializeNestedObjects() testObj.text = "test"; testObj.boolean = false; CPPUNIT_ASSERT_EQUAL( - "{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}}"s, + "{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[]}}"s, string(nestingObj.toJson().GetString())); NestingArray nestingArray; @@ -286,13 +308,13 @@ void JsonReflectorTests::testSerializeNestedObjects() nestingArray.testObjects.emplace_back(testObj); nestingArray.testObjects.back().number = 43; CPPUNIT_ASSERT_EQUAL( - "{\"name\":\"nesting2\",\"testObjects\":[{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{}},{\"number\":43,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}]}"s, + "{\"name\":\"nesting2\",\"testObjects\":[{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[]},{\"number\":43,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[]}]}"s, string(nestingArray.toJson().GetString())); vector nestedInVector; nestedInVector.emplace_back(testObj); CPPUNIT_ASSERT_EQUAL( - "[{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}]"s, + "[{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[]}]"s, string(JsonReflector::toJson(nestedInVector).GetString())); } @@ -320,7 +342,7 @@ void JsonReflectorTests::testSerializeUniquePtr() Writer jsonWriter(strbuf); doc.Accept(jsonWriter); CPPUNIT_ASSERT_EQUAL( - "[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}]"s, + "[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[]}]"s, string(strbuf.GetString())); } @@ -348,7 +370,7 @@ void JsonReflectorTests::testSerializeSharedPtr() Writer jsonWriter(strbuf); doc.Accept(jsonWriter); CPPUNIT_ASSERT_EQUAL( - "[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}]"s, + "[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[]}]"s, string(strbuf.GetString())); } @@ -413,8 +435,10 @@ void JsonReflectorTests::testDeserializePrimitives() */ void JsonReflectorTests::testDeserializeSimpleObjects() { - const TestObject testObj(TestObject::fromJson("{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":" - "false,\"someMap\":{\"a\":1,\"b\":2},\"someHash\":{\"c\":true,\"d\":false}}")); + const TestObject testObj( + TestObject::fromJson("{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":" + "false,\"someMap\":{\"a\":1,\"b\":2},\"someHash\":{\"c\":true,\"d\":false},\"someSet\":[\"a\",\"b\"],\"someMultiset\":[" + "\"a\",\"a\"],\"someUnorderedSet\":[\"a\",\"b\"],\"someUnorderedMultiset\":[\"a\",\"a\"]}")); CPPUNIT_ASSERT_EQUAL(42, testObj.number); CPPUNIT_ASSERT_EQUAL(3.141592653589793, testObj.number2); @@ -425,6 +449,10 @@ void JsonReflectorTests::testDeserializeSimpleObjects() CPPUNIT_ASSERT_EQUAL(expectedMap, testObj.someMap); const unordered_map expectedHash{ { "c", true }, { "d", false } }; CPPUNIT_ASSERT_EQUAL(expectedHash, testObj.someHash); + CPPUNIT_ASSERT_EQUAL(set({ "a", "b" }), testObj.someSet); + CPPUNIT_ASSERT_EQUAL(multiset({ "a", "a" }), testObj.someMultiset); + CPPUNIT_ASSERT_EQUAL(unordered_set({ "a", "b" }), testObj.someUnorderedSet); + CPPUNIT_ASSERT_EQUAL(unordered_multiset({ "a", "a" }), testObj.someUnorderedMultiset); } /*! @@ -532,7 +560,7 @@ void JsonReflectorTests::testHandlingParseError() } /*! - * \brief Tests whether JsonDeserializationError is thrown on type mismatch. + * \brief Tests whether errors are added on type mismatch and in other cases. */ void JsonReflectorTests::testHandlingTypeMismatch() { @@ -544,14 +572,25 @@ void JsonReflectorTests::testHandlingTypeMismatch() CPPUNIT_ASSERT_EQUAL(0_st, errors.size()); NestingObject::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":\"42\",\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":" - "\"test\",\"boolean\":false}}", + "\"test\",\"boolean\":false,\"someSet\":[\"a\",\"a\"],\"someMultiset\":[\"a\",\"a\"],\"someUnorderedSet\":[\"a\",\"a\"]," + "\"someUnorderedMultiset\":[\"a\",\"a\"]}}", &errors); - CPPUNIT_ASSERT_EQUAL(1_st, errors.size()); + CPPUNIT_ASSERT_EQUAL(3_st, errors.size()); CPPUNIT_ASSERT_EQUAL(JsonDeserializationErrorKind::TypeMismatch, errors.front().kind); CPPUNIT_ASSERT_EQUAL(JsonType::Number, errors.front().expectedType); CPPUNIT_ASSERT_EQUAL(JsonType::String, errors.front().actualType); CPPUNIT_ASSERT_EQUAL("number"s, string(errors.front().member)); CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors.front().record)); + CPPUNIT_ASSERT_EQUAL(JsonDeserializationErrorKind::UnexpectedDuplicate, errors[1].kind); + CPPUNIT_ASSERT_EQUAL(JsonType::Array, errors[1].expectedType); + CPPUNIT_ASSERT_EQUAL(JsonType::Array, errors[1].actualType); + CPPUNIT_ASSERT_EQUAL("someSet"s, string(errors[1].member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors[1].record)); + CPPUNIT_ASSERT_EQUAL(JsonDeserializationErrorKind::UnexpectedDuplicate, errors[2].kind); + CPPUNIT_ASSERT_EQUAL(JsonType::Array, errors[2].expectedType); + CPPUNIT_ASSERT_EQUAL(JsonType::Array, errors[2].actualType); + CPPUNIT_ASSERT_EQUAL("someUnorderedSet"s, string(errors[2].member)); + CPPUNIT_ASSERT_EQUAL("TestObject"s, string(errors[2].record)); errors.clear(); NestingObject::fromJson("{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":1,\"text\":"