diff --git a/README.md b/README.md index 6628a9f..ca2065a 100644 --- a/README.md +++ b/README.md @@ -13,18 +13,20 @@ generator. ## 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 | -| iteratables (`std::vector`, `std::list`, ...)| array | -| `std::tuple` | array | -| `std::unique_ptr`, `std::shared_ptr` | depends/null | +| 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 | ### Remarks * Raw pointer are not supported. This prevents @@ -34,7 +36,7 @@ The following table shows the mapping of supported C++ types to supported JSON t * For deserialization, iteratables must provide an `emplace_back` method. So deserialization of eg. `std::forward_list` is currently not supported. * The JSON type for smart pointer depends on the type the pointer refers to. It can also be `null`. -* See also TODOs.md. +* For custom (de)serialization, see the section below. ## Usage This example shows how the library can be used to make a `struct` serializable: diff --git a/TODOs.md b/TODOs.md index c67916b..c8a05c3 100644 --- a/TODOs.md +++ b/TODOs.md @@ -19,6 +19,6 @@ ## Library-only - [x] Support `std::unique_ptr` and `std::shared_ptr` -- [ ] Support `std::map` and `std::unordered_map` +- [x] Support `std::map` and `std::unordered_map` - [ ] Support `std::any` - [x] Support/document customized (de)serialization (eg. serialize some `DateTime` object to ISO string representation) diff --git a/lib/json/errorhandling.h b/lib/json/errorhandling.h index 304acd8..7eaa1ef 100644 --- a/lib/json/errorhandling.h +++ b/lib/json/errorhandling.h @@ -59,14 +59,20 @@ template , Traits::IsC return JsonType::String; } -template , Traits::Not>>...> constexpr JsonType jsonType() +template , + Traits::Not, Traits::IsSpecializationOf, + Traits::IsSpecializationOf>>>...> +constexpr JsonType jsonType() { return JsonType::Array; } template , std::is_floating_point, Traits::IsString, Traits::IsCString, - Traits::IsIteratable>...> + Traits::All, + Traits::Not, Traits::IsSpecializationOf, + Traits::IsSpecializationOf>>>>...> constexpr JsonType jsonType() { return JsonType::Object; diff --git a/lib/json/reflector.h b/lib/json/reflector.h index b5888d6..938a91a 100644 --- a/lib/json/reflector.h +++ b/lib/json/reflector.h @@ -16,9 +16,11 @@ #include #include +#include #include #include #include +#include #include "./errorhandling.h" @@ -75,6 +77,13 @@ template using IsCustomType = Traits::Not>; // define trait to check for custom structs/classes which are JSON serializable template using IsJsonSerializable = Traits::Any, Type>, AdaptedJsonSerializable>; +// define trait to check for map or hash +template +using IsMapOrHash = Traits::Any, Traits::IsSpecializationOf>; +template +using IsArray + = Traits::All, Traits::Not>, Traits::Not>>; + // define functions to "push" values to a RapidJSON array or object /*! @@ -175,8 +184,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, Traits::Not>>...> +template , Traits::HasSize>...> void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) { value.SetArray(); @@ -188,11 +196,9 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_ } /*! - * \brief Pushes the specified iteratable (eg. std::vector, std::list) to the specified value. + * \brief Pushes the specified iteratable list (eg. std::vector, std::list) to the specified value. */ -template , Traits::Not>, - Traits::Not>>...> +template , Traits::Not>>...> void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) { value.SetArray(); @@ -202,6 +208,19 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_ } } +/*! + * \brief Pushes the specified map (std::map, std::unordered_map) to the specified value. + */ +template >...> +void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator) +{ + value.SetObject(); + RAPIDJSON_NAMESPACE::Value::Object object(value.GetObject()); + for (const auto &item : reflectable) { + push(item.second, item.first.data(), object, allocator); + } +} + namespace Detail { /*! @@ -349,15 +368,13 @@ inline void pull( /*! * \brief Pulls the speciified \a reflectable which is an iteratable from the specified array. The \a reflectable is cleared before. */ -template , Traits::Not>>...> +template >...> void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors); /*! * \brief Pulls the speciified \a reflectable which is an iteratable without reserve() method from the specified value which is checked to contain an array. */ -template , Traits::Not>, - Traits::Not>>...> +template , Traits::Not>>...> void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors) { if (!value.IsArray()) { @@ -372,8 +389,7 @@ void pull(Type &reflectable, const rapidjson::GenericValue, Traits::IsReservable, Traits::Not>>...> +template , Traits::IsReservable>...> void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors) { if (!value.IsArray()) { @@ -390,7 +406,7 @@ void pull(Type &reflectable, const rapidjson::GenericValue, Traits::Not>>...> +template >...> void pull(Type &reflectable, rapidjson::GenericValue>::ConstArray array, JsonDeserializationErrors *errors) { // clear previous contents of the array @@ -414,6 +430,24 @@ void pull(Type &reflectable, rapidjson::GenericValue>...> +void pull(Type &reflectable, const rapidjson::GenericValue> &value, JsonDeserializationErrors *errors) +{ + if (!value.IsObject()) { + if (errors) { + errors->reportTypeMismatch(value.GetType()); + } + return; + } + auto obj = value.GetObject(); + for (auto i = obj.MemberBegin(), end = obj.MemberEnd(); i != end; ++i) { + pull(reflectable[i->name.GetString()], i->value, errors); + } +} + namespace Detail { /*! diff --git a/lib/tests/jsonreflector.cpp b/lib/tests/jsonreflector.cpp index 2ffe3d8..f994878 100644 --- a/lib/tests/jsonreflector.cpp +++ b/lib/tests/jsonreflector.cpp @@ -15,8 +15,10 @@ using TestUtilities::operator<<; // must be visible prior to the call site #include #include +#include #include #include +#include #include using namespace std; @@ -28,6 +30,14 @@ using namespace TestUtilities; using namespace TestUtilities::Literals; 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::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"); + /// \cond // define some structs for testing serialization @@ -37,6 +47,8 @@ struct TestObject : public JsonSerializable { vector numbers; string text; bool boolean; + map someMap; + unordered_map someHash; }; struct NestingObject : public JsonSerializable { @@ -72,6 +84,8 @@ template <> inline void push(const TestObject &reflectable, Value::O push(reflectable.numbers, "numbers", value, allocator); push(reflectable.text, "text", value, allocator); push(reflectable.boolean, "boolean", value, allocator); + push(reflectable.someMap, "someMap", value, allocator); + push(reflectable.someHash, "someHash", value, allocator); } template <> inline void push(const NestingObject &reflectable, Value::Object &value, Document::AllocatorType &allocator) @@ -99,6 +113,8 @@ inline void pull(TestObject &reflectable, const GenericValuecurrentRecord = previousRecord; } @@ -240,7 +256,10 @@ void JsonReflectorTests::testSerializeSimpleObjects() testObj.numbers = { 1, 2, 3, 4 }; testObj.text = "test"; testObj.boolean = false; - CPPUNIT_ASSERT_EQUAL("{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}"s, + testObj.someMap = { { "a", 1 }, { "b", 2 } }; + testObj.someHash = { { "c", true }, { "d", false } }; + 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, string(testObj.toJson().GetString())); } @@ -258,7 +277,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}}"s, + "{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}}"s, string(nestingObj.toJson().GetString())); NestingArray nestingArray; nestingArray.name = "nesting2"; @@ -266,7 +285,7 @@ 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},{\"number\":43,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false}]}"s, + "{\"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, string(nestingArray.toJson().GetString())); } @@ -293,7 +312,8 @@ void JsonReflectorTests::testSerializeUniquePtr() StringBuffer strbuf; 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}]"s, + CPPUNIT_ASSERT_EQUAL( + "[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}]"s, string(strbuf.GetString())); } @@ -320,7 +340,8 @@ void JsonReflectorTests::testSerializeSharedPtr() StringBuffer strbuf; 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}]"s, + CPPUNIT_ASSERT_EQUAL( + "[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{}}]"s, string(strbuf.GetString())); } @@ -379,14 +400,18 @@ 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}")); + 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}}")); CPPUNIT_ASSERT_EQUAL(42, testObj.number); CPPUNIT_ASSERT_EQUAL(3.141592653589793, testObj.number2); CPPUNIT_ASSERT_EQUAL(vector({ 1, 2, 3, 4 }), testObj.numbers); CPPUNIT_ASSERT_EQUAL("test"s, testObj.text); CPPUNIT_ASSERT_EQUAL(false, testObj.boolean); + const map expectedMap{ { "a", 1 }, { "b", 2 } }; + CPPUNIT_ASSERT_EQUAL(expectedMap, testObj.someMap); + const unordered_map expectedHash{ { "c", true }, { "d", false } }; + CPPUNIT_ASSERT_EQUAL(expectedHash, testObj.someHash); } /*!