Support multimap and unordered_multimap

The previously (undocumented) implementation used non-unique
keys in JSON objects. This is not strictly forbidden by the
RFC but not recommended. Multiple values are now stored within
an array instead.
This commit is contained in:
Martchus 2019-12-27 01:40:34 +01:00
parent 022a174028
commit 450588af89
4 changed files with 84 additions and 34 deletions

View File

@ -49,22 +49,22 @@ 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 |
| 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 |
| `std::variant` | 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`, `std::multimap`, `std::unordered_multimap` | object |
| `std::variant` | object |
| `JsonSerializable` | object |
### Remarks
* Raw pointer are not supported. This prevents
@ -85,7 +85,11 @@ The following table shows the mapping of supported C++ types to supported JSON t
* It is possible to treat custom types as set/map using the macro `REFLECTIVE_RAPIDJSON_TREAT_AS_MAP_OR_HASH`,
`REFLECTIVE_RAPIDJSON_TREAT_AS_MULTI_MAP_OR_HASH`, `REFLECTIVE_RAPIDJSON_TREAT_AS_SET` or
`REFLECTIVE_RAPIDJSON_TREAT_AS_MULTI_SET`.
* The key type of `std::map` and `std::unordered_map` must be `std::string`.
* The key type of `std::map`, `std::unordered_map`, `std::multimap` and `std::unordered_multimap` must be
`std::string`.
* An array is used to represent the multiple values of an `std::multimap` and `std::unordered_multimap` (for
consistency also when there is only one value present). This is because the JSON RFC says that
"The names within an object SHOULD be unique".
* An `std::variant` is represented by an object like `{"index": ..., "data": ...}` where `index` is the
zero-based index of the alternative held by the variant and `data` the value held by the variant. The
type of `data` is `null` for `std::monostate` and otherwise deduced as usual.

View File

@ -17,9 +17,13 @@
#include <rapidjson/writer.h>
#include <limits>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
#include <variant>
#include "./errorhandling.h"
@ -213,10 +217,9 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_
}
/*!
* \brief Pushes the specified map (std::map, std::unordered_map) or multimap (std::multimap, std::unordered_multimap) to the
* specified value.
* \brief Pushes the specified map (std::map, std::unordered_map) to the specified value.
*/
template <typename Type, Traits::EnableIfAny<IsMapOrHash<Type>, IsMultiMapOrHash<Type>> * = nullptr>
template <typename Type, Traits::EnableIfAny<IsMapOrHash<Type>> * = nullptr>
void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)
{
value.SetObject();
@ -226,6 +229,27 @@ void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_
}
}
/*!
* \brief Pushes the specified multimap (std::multimap, std::unordered_multimap) to the specified value.
*/
template <typename Type, Traits::EnableIfAny<IsMultiMapOrHash<Type>> * = nullptr>
void push(const Type &reflectable, RAPIDJSON_NAMESPACE::Value &value, RAPIDJSON_NAMESPACE::Document::AllocatorType &allocator)
{
value.SetObject();
for (const auto &item : reflectable) {
auto arrayValue = RAPIDJSON_NAMESPACE::Value(RAPIDJSON_NAMESPACE::Type::kArrayType);
const auto memberName = RAPIDJSON_NAMESPACE::Value::StringRefType(item.first.data(), rapidJsonSize(item.first.size()));
const auto existingMember = value.FindMember(memberName);
if (existingMember != value.MemberEnd() && existingMember->value.GetType() == RAPIDJSON_NAMESPACE::Type::kArrayType) {
arrayValue = existingMember->value;
} else {
value.AddMember(memberName, arrayValue, allocator);
}
RAPIDJSON_NAMESPACE::Value::Array array = arrayValue.GetArray();
push(item.second, array, allocator);
}
}
namespace Detail {
/*!
@ -686,8 +710,16 @@ void pull(Type &reflectable, const rapidjson::GenericValue<RAPIDJSON_NAMESPACE::
}
auto obj = value.GetObject();
for (auto i = obj.MemberBegin(), end = obj.MemberEnd(); i != end; ++i) {
auto insertedIterator = reflectable.insert(typename Type::value_type(i->name.GetString(), typename Type::mapped_type()));
pull(insertedIterator->second, i->value, errors);
if (i->value.GetType() != RAPIDJSON_NAMESPACE::kArrayType) {
auto insertedIterator = reflectable.insert(typename Type::value_type(i->name.GetString(), typename Type::mapped_type()));
pull(insertedIterator->second, i->value, errors);
continue;
}
const auto array = i->value.GetArray();
for (const auto &value : array) {
auto insertedIterator = reflectable.insert(typename Type::value_type(i->name.GetString(), typename Type::mapped_type()));
pull(insertedIterator->second, value, errors);
}
}
}

View File

@ -52,6 +52,8 @@ struct TestObject : public JsonSerializable<TestObject> {
bool boolean;
map<string, int> someMap;
unordered_map<string, bool> someHash;
multimap<string, int> someMultimap;
unordered_multimap<string, int> someMultiHash;
set<string> someSet;
multiset<string> someMultiset;
unordered_set<string> someUnorderedSet;
@ -84,6 +86,8 @@ template <> inline void push<TestObject>(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.someMultimap, "someMultimap", value, allocator);
push(reflectable.someMultiHash, "someMultiHash", value, allocator);
push(reflectable.someSet, "someSet", value, allocator);
push(reflectable.someMultiset, "someMultiset", value, allocator);
push(reflectable.someUnorderedSet, "someUnorderedSet", value, allocator);
@ -120,6 +124,8 @@ inline void pull<TestObject>(TestObject &reflectable, const GenericValue<UTF8<ch
pull(reflectable.boolean, "boolean", value, errors);
pull(reflectable.someMap, "someMap", value, errors);
pull(reflectable.someHash, "someHash", value, errors);
pull(reflectable.someMultimap, "someMultimap", value, errors);
pull(reflectable.someMultiHash, "someMultiHash", value, errors);
pull(reflectable.someSet, "someSet", value, errors);
pull(reflectable.someMultiset, "someMultiset", value, errors);
pull(reflectable.someUnorderedSet, "someUnorderedSet", value, errors);
@ -270,6 +276,8 @@ void JsonReflectorTests::testSerializeSimpleObjects()
testObj.boolean = false;
testObj.someMap = { { "a", 1 }, { "b", 2 } };
testObj.someHash = { { "c", true }, { "d", false } };
testObj.someMultimap = { { "a", 1 }, { "a", 2 }, { "b", 3 } };
testObj.someMultiHash = { { "a", 1 } };
testObj.someSet = { "a", "b", "c" };
testObj.someMultiset = { "a", "b", "b" };
testObj.someUnorderedSet = { "a" };
@ -278,7 +286,7 @@ void JsonReflectorTests::testSerializeSimpleObjects()
testObj.anotherVariant = "foo";
testObj.yetAnotherVariant = 42;
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},\"someSet\":[\"a\",\"b\",\"c\"],\"someMultiset\":[\"a\",\"b\",\"b\"],\"someUnorderedSet\":[\"a\"],\"someUnorderedMultiset\":[\"b\",\"b\",\"b\"],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"foo\"},\"yetAnotherVariant\":{\"index\":1,\"data\":42}}"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},\"someMultimap\":{\"a\":[1,2],\"b\":[3]},\"someMultiHash\":{\"a\":[1]},\"someSet\":[\"a\",\"b\",\"c\"],\"someMultiset\":[\"a\",\"b\",\"b\"],\"someUnorderedSet\":[\"a\"],\"someUnorderedMultiset\":[\"b\",\"b\",\"b\"],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"foo\"},\"yetAnotherVariant\":{\"index\":1,\"data\":42}}"s,
string(testObj.toJson().GetString()));
}
@ -296,7 +304,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\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}}"s,
"{\"name\":\"nesting\",\"testObj\":{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someMultimap\":{},\"someMultiHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}}"s,
string(nestingObj.toJson().GetString()));
NestingArray nestingArray;
@ -305,13 +313,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\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}},{\"number\":43,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]}"s,
"{\"name\":\"nesting2\",\"testObjects\":[{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someMultimap\":{},\"someMultiHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}},{\"number\":43,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someMultimap\":{},\"someMultiHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]}"s,
string(nestingArray.toJson().GetString()));
vector<TestObject> nestedInVector;
nestedInVector.emplace_back(testObj);
CPPUNIT_ASSERT_EQUAL(
"[{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]"s,
"[{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"test\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someMultimap\":{},\"someMultiHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]"s,
string(JsonReflector::toJson(nestedInVector).GetString()));
}
@ -339,7 +347,7 @@ void JsonReflectorTests::testSerializeUniquePtr()
Writer<StringBuffer> 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\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]"s,
"[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someMultimap\":{},\"someMultiHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]"s,
string(strbuf.GetString()));
}
@ -367,7 +375,7 @@ void JsonReflectorTests::testSerializeSharedPtr()
Writer<StringBuffer> 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\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]"s,
"[\"foo\",null,{\"number\":42,\"number2\":3.141592653589793,\"numbers\":[1,2,3,4],\"text\":\"bar\",\"boolean\":false,\"someMap\":{},\"someHash\":{},\"someMultimap\":{},\"someMultiHash\":{},\"someSet\":[],\"someMultiset\":[],\"someUnorderedSet\":[],\"someUnorderedMultiset\":[],\"someVariant\":{\"index\":0,\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"\"},\"yetAnotherVariant\":{\"index\":0,\"data\":\"\"}}]"s,
string(strbuf.GetString()));
}
@ -432,11 +440,13 @@ 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},\"someSet\":[\"a\",\"b\"],\"someMultiset\":["
"\"a\",\"a\"],\"someUnorderedSet\":[\"a\",\"b\"],\"someUnorderedMultiset\":[\"a\",\"a\"],\"someVariant\":{\"index\":0,"
"\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"foo\"},\"yetAnotherVariant\":{\"index\":1,\"data\":42}}"));
const auto 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},\"someMultimap\":{\"a\":[1,2],\"b\":[3]},"
"\"someMultiHash\":{\"a\":[4,5],\"b\":[6]},"
"\"someSet\":[\"a\",\"b\"],\"someMultiset\":["
"\"a\",\"a\"],\"someUnorderedSet\":[\"a\",\"b\"],\"someUnorderedMultiset\":[\"a\",\"a\"],\"someVariant\":{\"index\":0,"
"\"data\":null},\"anotherVariant\":{\"index\":0,\"data\":\"foo\"},\"yetAnotherVariant\":{\"index\":1,\"data\":42}}");
CPPUNIT_ASSERT_EQUAL(42, testObj.number);
CPPUNIT_ASSERT_EQUAL(3.141592653589793, testObj.number2);
@ -447,6 +457,10 @@ void JsonReflectorTests::testDeserializeSimpleObjects()
CPPUNIT_ASSERT_EQUAL(expectedMap, testObj.someMap);
const unordered_map<string, bool> expectedHash{ { "c", true }, { "d", false } };
CPPUNIT_ASSERT_EQUAL(expectedHash, testObj.someHash);
const multimap<string, int> expectedMultiMap{ { "a", 1 }, { "a", 2 }, { "b", 3 } };
CPPUNIT_ASSERT_EQUAL(expectedMultiMap, testObj.someMultimap);
const unordered_multimap<string, int> expectedUnorderedMultiMap{ { "a", 4 }, { "a", 5 }, { "b", 6 } };
CPPUNIT_ASSERT_EQUAL(expectedUnorderedMultiMap, testObj.someMultiHash);
CPPUNIT_ASSERT_EQUAL(set<string>({ "a", "b" }), testObj.someSet);
CPPUNIT_ASSERT_EQUAL(multiset<string>({ "a", "a" }), testObj.someMultiset);
CPPUNIT_ASSERT_EQUAL(unordered_set<string>({ "a", "b" }), testObj.someUnorderedSet);

View File

@ -59,7 +59,7 @@ using IsArrayOrSet = Traits::Any<Traits::All<Traits::IsIteratable<Type>, Traits:
TreatAsSet<Type>, TreatAsMultiSet<Type>>;
template <typename Type>
using IsArray = Traits::All<Traits::IsIteratable<Type>, Traits::Not<Traits::IsSpecializationOf<Type, std::basic_string>>,
Traits::Not<IsMapOrHash<Type>>, Traits::Not<IsSet<Type>>, Traits::Not<IsMultiSet<Type>>>;
Traits::Not<IsMapOrHash<Type>>, Traits::Not<IsMultiMapOrHash<Type>>, Traits::Not<IsSet<Type>>, Traits::Not<IsMultiSet<Type>>>;
template <typename Type>
using IsIteratableExceptString = Traits::All<Traits::IsIteratable<Type>, Traits::Not<Traits::IsSpecializationOf<Type, std::basic_string>>>;
template <typename Type> using IsVariant = Traits::All<Traits::IsSpecializationOf<Type, std::variant>>;