mirror of
https://github.com/nlohmann/json.git
synced 2026-10-09 16:07:14 +00:00
Restore v3.12.0 support for custom object key types
Custom object_t types whose key_type is not string_t compiled with v3.12.0 for several APIs that unreleased changes broke: - to_bson failed for every custom key type (#5553 kept a const string_t* to the key); the nested entry's header is now written where the entry is found. - Copying deep values (and parse, merge_patch, update, insert) required operator== on keys (#5389); keys without one are now paired via find(). - to_cbor/to_msgpack required an implicit conversion to string_t (#5746, #5328); keys without one go through a temporary basic_json again. - at() required a conversion to string_t for its error message (#5727); other keys are passed to concat() unchanged again. The new unit-custom-object-key-type.cpp covers five key types with different capabilities. Signed-off-by: Niels Lohmann <mail@nlohmann.me>
This commit is contained in:
6 files changed
+672
-48
No files matched your search
@@ -26,8 +26,7 @@ To store objects in C++, a type is defined by the template parameters described
|
||||
|
||||
`StringType`
|
||||
: the type of the keys or names (e.g., `std::string`). The comparison function `std::less<StringType>` is used to
|
||||
order elements inside the container. `object_t::key_type` must be implicitly convertible to `string_t` (required by the
|
||||
binary formats).
|
||||
order elements inside the container.
|
||||
|
||||
`AllocatorType`
|
||||
: the allocator to use for objects (e.g., `std::allocator`)
|
||||
|
||||
@@ -172,6 +172,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
|
||||
template<typename T>
|
||||
using detect_key_compare = typename T::key_compare;
|
||||
|
||||
// detects whether two values of type T can be compared with operator==
|
||||
template<typename T>
|
||||
using detect_equal_comparable = decltype(static_cast<bool>(std::declval<const T&>() == std::declval<const T&>()));
|
||||
|
||||
// obtains the actual object key comparator: object_t::key_compare if the
|
||||
// object type defines it, and default_object_comparator_t otherwise
|
||||
//
|
||||
|
||||
@@ -307,12 +307,6 @@ class binary_writer
|
||||
|
||||
case value_t::object:
|
||||
{
|
||||
static_assert(
|
||||
std::is_convertible <
|
||||
typename BasicJsonType::object_t::key_type,
|
||||
string_t >::value,
|
||||
"object_t::key_type must be implicitly convertible to string_t");
|
||||
|
||||
// step 1: write control byte and the object size
|
||||
write_cbor_head(0xA0, j.m_data.m_value.object->size());
|
||||
|
||||
@@ -320,7 +314,7 @@ class binary_writer
|
||||
{
|
||||
// el.first is written directly (not via a temporary
|
||||
// basic_json), with the object as diagnostics context
|
||||
write_cbor_string(el.first, j);
|
||||
write_cbor_key(el.first, j);
|
||||
write_cbor(el.second, depth + 1);
|
||||
}
|
||||
break;
|
||||
@@ -585,12 +579,6 @@ class binary_writer
|
||||
|
||||
case value_t::object:
|
||||
{
|
||||
static_assert(
|
||||
std::is_convertible <
|
||||
typename BasicJsonType::object_t::key_type,
|
||||
string_t >::value,
|
||||
"object_t::key_type must be implicitly convertible to string_t");
|
||||
|
||||
// step 1: write control byte and the object size
|
||||
write_msgpack_object_prefix(j.m_data.m_value.object->size(), j);
|
||||
|
||||
@@ -598,7 +586,7 @@ class binary_writer
|
||||
{
|
||||
// as in write_cbor, el.first is written directly with the
|
||||
// object as diagnostics context
|
||||
write_msgpack_string(el.first, j);
|
||||
write_msgpack_key(el.first, j);
|
||||
write_msgpack(el.second, depth + 1);
|
||||
}
|
||||
break;
|
||||
@@ -984,7 +972,7 @@ class binary_writer
|
||||
|
||||
// the key is written directly (not via a temporary basic_json),
|
||||
// with the object as diagnostics context, as in write_cbor
|
||||
write_cbor_string(current.object_it->first, *current.value);
|
||||
write_cbor_key(current.object_it->first, *current.value);
|
||||
const BasicJsonType* child = &(current.object_it->second);
|
||||
++stack.back().object_it;
|
||||
write_cbor_value_or_push(*child, stack);
|
||||
@@ -1061,7 +1049,7 @@ class binary_writer
|
||||
|
||||
// as in write_cbor_iterative, the key is written directly with
|
||||
// the object as diagnostics context
|
||||
write_msgpack_string(current.object_it->first, *current.value);
|
||||
write_msgpack_key(current.object_it->first, *current.value);
|
||||
const BasicJsonType* child = &(current.object_it->second);
|
||||
++stack.back().object_it;
|
||||
write_msgpack_value_or_push(*child, stack);
|
||||
@@ -1828,7 +1816,6 @@ class binary_writer
|
||||
{
|
||||
// write entries until the current object or array is done, or an
|
||||
// entry is an object or array itself
|
||||
const string_t* nested_name = nullptr;
|
||||
const BasicJsonType* nested = nullptr;
|
||||
if (current.value->is_object())
|
||||
{
|
||||
@@ -1839,7 +1826,8 @@ class binary_writer
|
||||
++current.member;
|
||||
if (el.second.is_structured())
|
||||
{
|
||||
nested_name = &el.first;
|
||||
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
|
||||
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
|
||||
nested = &el.second;
|
||||
}
|
||||
else
|
||||
@@ -1858,7 +1846,8 @@ class binary_writer
|
||||
++current.index;
|
||||
if (el.is_structured())
|
||||
{
|
||||
nested_name = &index_name;
|
||||
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
|
||||
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
|
||||
nested = ⪙
|
||||
}
|
||||
else
|
||||
@@ -1870,8 +1859,6 @@ class binary_writer
|
||||
|
||||
if (nested != nullptr)
|
||||
{
|
||||
write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04);
|
||||
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
|
||||
parents.push_back(std::move(current));
|
||||
current = bson_frame(nested);
|
||||
continue;
|
||||
@@ -1939,6 +1926,43 @@ class binary_writer
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief write an object key as a CBOR text string
|
||||
|
||||
A key convertible to string_t is written directly. Other key types (only
|
||||
an explicit conversion, or only a to_json overload) go through a temporary
|
||||
basic_json, as in version 3.12.0; the temporary is then the diagnostics
|
||||
context for strict UTF-8 checks.
|
||||
*/
|
||||
template<typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
|
||||
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
|
||||
{
|
||||
write_cbor_string(key, context);
|
||||
}
|
||||
|
||||
template < typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
|
||||
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
|
||||
{
|
||||
write_cbor(BasicJsonType(key));
|
||||
}
|
||||
|
||||
/// @brief write an object key as a MessagePack str, as in @ref write_cbor_key
|
||||
template<typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
|
||||
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
|
||||
{
|
||||
write_msgpack_string(key, context);
|
||||
}
|
||||
|
||||
template < typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
|
||||
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
|
||||
{
|
||||
write_msgpack(BasicJsonType(key));
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief write a CBOR text string
|
||||
|
||||
|
||||
@@ -1421,6 +1421,24 @@ public:
|
||||
return create<object_t>(first, last);
|
||||
}
|
||||
|
||||
/// @brief compare two object keys for equality, if the key type supports it
|
||||
/// @note object_t only needs operator< for its keys (std::map), so operator==
|
||||
/// may not exist; the keys are then reported as different, which makes
|
||||
/// copy_object_level pair the values via object_t::find()
|
||||
template<typename K = typename object_t::key_type,
|
||||
detail::enable_if_t<detail::is_detected<detail::detect_equal_comparable, K>::value, int> = 0>
|
||||
static bool copy_keys_equal(const K& a, const K& b)
|
||||
{
|
||||
return a == b;
|
||||
}
|
||||
|
||||
template < typename K = typename object_t::key_type,
|
||||
detail::enable_if_t < !detail::is_detected<detail::detect_equal_comparable, K>::value, int > = 0 >
|
||||
static bool copy_keys_equal(const K& /*a*/, const K& /*b*/)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// @brief create the copy of the object @a src in @a dst
|
||||
/// @note structured values are appended to @a worklist instead
|
||||
static void copy_object_level(const basic_json& src, basic_json& dst,
|
||||
@@ -1453,7 +1471,7 @@ public:
|
||||
auto src_it = src_object.cbegin();
|
||||
for (auto& element : *dst.m_data.m_value.object)
|
||||
{
|
||||
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first))
|
||||
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first)))
|
||||
{
|
||||
copy_shallow(src_it->second, element.second, worklist);
|
||||
++src_it;
|
||||
@@ -3330,11 +3348,27 @@ public:
|
||||
// std::map or ordered_map) never moves from its argument, so key is still
|
||||
// valid here regardless of whether KeyType was deduced as an rvalue reference
|
||||
// NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved)
|
||||
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j));
|
||||
JSON_THROW(out_of_range::create(403, detail::concat("key '", key_for_message(key), "' not found"), &j));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/// @brief key as it is passed to detail::concat for an error message
|
||||
/// @note string_t is used where it can be constructed from the key; other
|
||||
/// key types are passed through unchanged, as concat only needs
|
||||
/// data() and size() of them
|
||||
template<typename KeyType, detail::enable_if_t<std::is_constructible<string_t, const KeyType&>::value, int> = 0>
|
||||
static string_t key_for_message(const KeyType& key)
|
||||
{
|
||||
return string_t(key);
|
||||
}
|
||||
|
||||
template < typename KeyType, detail::enable_if_t < !std::is_constructible<string_t, const KeyType&>::value, int > = 0 >
|
||||
static const KeyType & key_for_message(const KeyType& key)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
/// @brief checked array element access used by the at() overloads taking an index
|
||||
/// @throw type_error.304 if @a j is not an array
|
||||
/// @throw out_of_range.401 if @a idx is out of range
|
||||
|
||||
@@ -4171,6 +4171,10 @@ struct has_to_json < BasicJsonType, T, enable_if_t < !is_basic_json<T>::value >>
|
||||
template<typename T>
|
||||
using detect_key_compare = typename T::key_compare;
|
||||
|
||||
// detects whether two values of type T can be compared with operator==
|
||||
template<typename T>
|
||||
using detect_equal_comparable = decltype(static_cast<bool>(std::declval<const T&>() == std::declval<const T&>()));
|
||||
|
||||
// obtains the actual object key comparator: object_t::key_compare if the
|
||||
// object type defines it, and default_object_comparator_t otherwise
|
||||
//
|
||||
@@ -21812,12 +21816,6 @@ class binary_writer
|
||||
|
||||
case value_t::object:
|
||||
{
|
||||
static_assert(
|
||||
std::is_convertible <
|
||||
typename BasicJsonType::object_t::key_type,
|
||||
string_t >::value,
|
||||
"object_t::key_type must be implicitly convertible to string_t");
|
||||
|
||||
// step 1: write control byte and the object size
|
||||
write_cbor_head(0xA0, j.m_data.m_value.object->size());
|
||||
|
||||
@@ -21825,7 +21823,7 @@ class binary_writer
|
||||
{
|
||||
// el.first is written directly (not via a temporary
|
||||
// basic_json), with the object as diagnostics context
|
||||
write_cbor_string(el.first, j);
|
||||
write_cbor_key(el.first, j);
|
||||
write_cbor(el.second, depth + 1);
|
||||
}
|
||||
break;
|
||||
@@ -22090,12 +22088,6 @@ class binary_writer
|
||||
|
||||
case value_t::object:
|
||||
{
|
||||
static_assert(
|
||||
std::is_convertible <
|
||||
typename BasicJsonType::object_t::key_type,
|
||||
string_t >::value,
|
||||
"object_t::key_type must be implicitly convertible to string_t");
|
||||
|
||||
// step 1: write control byte and the object size
|
||||
write_msgpack_object_prefix(j.m_data.m_value.object->size(), j);
|
||||
|
||||
@@ -22103,7 +22095,7 @@ class binary_writer
|
||||
{
|
||||
// as in write_cbor, el.first is written directly with the
|
||||
// object as diagnostics context
|
||||
write_msgpack_string(el.first, j);
|
||||
write_msgpack_key(el.first, j);
|
||||
write_msgpack(el.second, depth + 1);
|
||||
}
|
||||
break;
|
||||
@@ -22489,7 +22481,7 @@ class binary_writer
|
||||
|
||||
// the key is written directly (not via a temporary basic_json),
|
||||
// with the object as diagnostics context, as in write_cbor
|
||||
write_cbor_string(current.object_it->first, *current.value);
|
||||
write_cbor_key(current.object_it->first, *current.value);
|
||||
const BasicJsonType* child = &(current.object_it->second);
|
||||
++stack.back().object_it;
|
||||
write_cbor_value_or_push(*child, stack);
|
||||
@@ -22566,7 +22558,7 @@ class binary_writer
|
||||
|
||||
// as in write_cbor_iterative, the key is written directly with
|
||||
// the object as diagnostics context
|
||||
write_msgpack_string(current.object_it->first, *current.value);
|
||||
write_msgpack_key(current.object_it->first, *current.value);
|
||||
const BasicJsonType* child = &(current.object_it->second);
|
||||
++stack.back().object_it;
|
||||
write_msgpack_value_or_push(*child, stack);
|
||||
@@ -23333,7 +23325,6 @@ class binary_writer
|
||||
{
|
||||
// write entries until the current object or array is done, or an
|
||||
// entry is an object or array itself
|
||||
const string_t* nested_name = nullptr;
|
||||
const BasicJsonType* nested = nullptr;
|
||||
if (current.value->is_object())
|
||||
{
|
||||
@@ -23344,7 +23335,8 @@ class binary_writer
|
||||
++current.member;
|
||||
if (el.second.is_structured())
|
||||
{
|
||||
nested_name = &el.first;
|
||||
write_bson_entry_header(el.first, el.second.is_object() ? 0x03 : 0x04);
|
||||
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
|
||||
nested = &el.second;
|
||||
}
|
||||
else
|
||||
@@ -23363,7 +23355,8 @@ class binary_writer
|
||||
++current.index;
|
||||
if (el.is_structured())
|
||||
{
|
||||
nested_name = &index_name;
|
||||
write_bson_entry_header(index_name, el.is_object() ? 0x03 : 0x04);
|
||||
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
|
||||
nested = ⪙
|
||||
}
|
||||
else
|
||||
@@ -23375,8 +23368,6 @@ class binary_writer
|
||||
|
||||
if (nested != nullptr)
|
||||
{
|
||||
write_bson_entry_header(*nested_name, nested->is_object() ? 0x03 : 0x04);
|
||||
write_number<std::int32_t>(to_bson_length(nested_sizes[next_size++]), true);
|
||||
parents.push_back(std::move(current));
|
||||
current = bson_frame(nested);
|
||||
continue;
|
||||
@@ -23444,6 +23435,43 @@ class binary_writer
|
||||
}
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief write an object key as a CBOR text string
|
||||
|
||||
A key convertible to string_t is written directly. Other key types (only
|
||||
an explicit conversion, or only a to_json overload) go through a temporary
|
||||
basic_json, as in version 3.12.0; the temporary is then the diagnostics
|
||||
context for strict UTF-8 checks.
|
||||
*/
|
||||
template<typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
|
||||
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
|
||||
{
|
||||
write_cbor_string(key, context);
|
||||
}
|
||||
|
||||
template < typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
|
||||
void write_cbor_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
|
||||
{
|
||||
write_cbor(BasicJsonType(key));
|
||||
}
|
||||
|
||||
/// @brief write an object key as a MessagePack str, as in @ref write_cbor_key
|
||||
template<typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t<std::is_convertible<Key, string_t>::value, int> = 0>
|
||||
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& context)
|
||||
{
|
||||
write_msgpack_string(key, context);
|
||||
}
|
||||
|
||||
template < typename Key = typename BasicJsonType::object_t::key_type,
|
||||
enable_if_t < !std::is_convertible<Key, string_t>::value, int > = 0 >
|
||||
void write_msgpack_key(const typename BasicJsonType::object_t::key_type& key, const BasicJsonType& /*context*/)
|
||||
{
|
||||
write_msgpack(BasicJsonType(key));
|
||||
}
|
||||
|
||||
/*!
|
||||
@brief write a CBOR text string
|
||||
|
||||
@@ -29160,6 +29188,24 @@ public:
|
||||
return create<object_t>(first, last);
|
||||
}
|
||||
|
||||
/// @brief compare two object keys for equality, if the key type supports it
|
||||
/// @note object_t only needs operator< for its keys (std::map), so operator==
|
||||
/// may not exist; the keys are then reported as different, which makes
|
||||
/// copy_object_level pair the values via object_t::find()
|
||||
template<typename K = typename object_t::key_type,
|
||||
detail::enable_if_t<detail::is_detected<detail::detect_equal_comparable, K>::value, int> = 0>
|
||||
static bool copy_keys_equal(const K& a, const K& b)
|
||||
{
|
||||
return a == b;
|
||||
}
|
||||
|
||||
template < typename K = typename object_t::key_type,
|
||||
detail::enable_if_t < !detail::is_detected<detail::detect_equal_comparable, K>::value, int > = 0 >
|
||||
static bool copy_keys_equal(const K& /*a*/, const K& /*b*/)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
/// @brief create the copy of the object @a src in @a dst
|
||||
/// @note structured values are appended to @a worklist instead
|
||||
static void copy_object_level(const basic_json& src, basic_json& dst,
|
||||
@@ -29192,7 +29238,7 @@ public:
|
||||
auto src_it = src_object.cbegin();
|
||||
for (auto& element : *dst.m_data.m_value.object)
|
||||
{
|
||||
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && src_it->first == element.first))
|
||||
if (JSON_HEDLEY_LIKELY(src_it != src_object.cend() && copy_keys_equal(src_it->first, element.first)))
|
||||
{
|
||||
copy_shallow(src_it->second, element.second, worklist);
|
||||
++src_it;
|
||||
@@ -31069,11 +31115,27 @@ public:
|
||||
// std::map or ordered_map) never moves from its argument, so key is still
|
||||
// valid here regardless of whether KeyType was deduced as an rvalue reference
|
||||
// NOLINTNEXTLINE(bugprone-use-after-move,hicpp-invalid-access-moved)
|
||||
JSON_THROW(out_of_range::create(403, detail::concat("key '", string_t(key), "' not found"), &j));
|
||||
JSON_THROW(out_of_range::create(403, detail::concat("key '", key_for_message(key), "' not found"), &j));
|
||||
}
|
||||
return it->second;
|
||||
}
|
||||
|
||||
/// @brief key as it is passed to detail::concat for an error message
|
||||
/// @note string_t is used where it can be constructed from the key; other
|
||||
/// key types are passed through unchanged, as concat only needs
|
||||
/// data() and size() of them
|
||||
template<typename KeyType, detail::enable_if_t<std::is_constructible<string_t, const KeyType&>::value, int> = 0>
|
||||
static string_t key_for_message(const KeyType& key)
|
||||
{
|
||||
return string_t(key);
|
||||
}
|
||||
|
||||
template < typename KeyType, detail::enable_if_t < !std::is_constructible<string_t, const KeyType&>::value, int > = 0 >
|
||||
static const KeyType & key_for_message(const KeyType& key)
|
||||
{
|
||||
return key;
|
||||
}
|
||||
|
||||
/// @brief checked array element access used by the at() overloads taking an index
|
||||
/// @throw type_error.304 if @a j is not an array
|
||||
/// @throw out_of_range.401 if @a idx is out of range
|
||||
|
||||
@@ -0,0 +1,501 @@
|
||||
// __ _____ _____ _____
|
||||
// __| | __| | | | JSON for Modern C++ (supporting code)
|
||||
// | | |__ | | | | | | version 3.12.0
|
||||
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
||||
//
|
||||
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
||||
// SPDX-License-Identifier: MIT
|
||||
|
||||
#include "doctest_compatibility.h"
|
||||
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
// Object types with a user-defined key type. The key types differ in what they
|
||||
// offer to the library: a conversion to std::string (implicit or explicit), a
|
||||
// comparison with ==, a to_json overload, or a c_str() member.
|
||||
|
||||
namespace custom_key_test
|
||||
{
|
||||
class key_base
|
||||
{
|
||||
public:
|
||||
key_base() = default;
|
||||
|
||||
key_base(const char* value)
|
||||
: m_value(value)
|
||||
{}
|
||||
|
||||
key_base(std::string value)
|
||||
: m_value(std::move(value))
|
||||
{}
|
||||
|
||||
// Required by JSON_DIAGNOSTICS, which reads object keys through data()
|
||||
// when building the path of an exception.
|
||||
const char* data() const noexcept
|
||||
{
|
||||
return m_value.data();
|
||||
}
|
||||
|
||||
friend bool operator<(const key_base& lhs, const key_base& rhs)
|
||||
{
|
||||
return lhs.m_value < rhs.m_value;
|
||||
}
|
||||
|
||||
protected:
|
||||
std::string m_value;
|
||||
};
|
||||
|
||||
// implicit conversion to std::string and operator==
|
||||
class key_full : public key_base
|
||||
{
|
||||
public:
|
||||
key_full() = default;
|
||||
using key_base::key_base;
|
||||
|
||||
operator std::string() const
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
|
||||
friend bool operator==(const key_full& lhs, const key_full& rhs)
|
||||
{
|
||||
return lhs.m_value == rhs.m_value;
|
||||
}
|
||||
};
|
||||
|
||||
// implicit conversion to std::string, but no operator==
|
||||
class key_no_eq : public key_base
|
||||
{
|
||||
public:
|
||||
key_no_eq() = default;
|
||||
using key_base::key_base;
|
||||
|
||||
operator std::string() const
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
};
|
||||
|
||||
// explicit conversion to std::string, no operator==
|
||||
class key_explicit : public key_base
|
||||
{
|
||||
public:
|
||||
key_explicit() = default;
|
||||
using key_base::key_base;
|
||||
|
||||
explicit operator std::string() const
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
};
|
||||
|
||||
// no conversion at all, only a to_json overload, no operator==
|
||||
class key_to_json : public key_base
|
||||
{
|
||||
public:
|
||||
key_to_json() = default;
|
||||
using key_base::key_base;
|
||||
|
||||
const std::string& value() const
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
};
|
||||
|
||||
template<typename BasicJsonType>
|
||||
void to_json(BasicJsonType& j, const key_to_json& k)
|
||||
{
|
||||
j = k.value();
|
||||
}
|
||||
|
||||
// like key_to_json, but with size() and c_str()
|
||||
class key_c_str : public key_base
|
||||
{
|
||||
public:
|
||||
key_c_str() = default;
|
||||
using key_base::key_base;
|
||||
|
||||
const std::string& value() const
|
||||
{
|
||||
return m_value;
|
||||
}
|
||||
|
||||
std::size_t size() const
|
||||
{
|
||||
return m_value.size();
|
||||
}
|
||||
|
||||
const char* c_str() const
|
||||
{
|
||||
return m_value.c_str();
|
||||
}
|
||||
};
|
||||
|
||||
template<typename BasicJsonType>
|
||||
void to_json(BasicJsonType& j, const key_c_str& k)
|
||||
{
|
||||
j = k.value();
|
||||
}
|
||||
|
||||
// std::map with key type K, ignoring the key type basic_json passes
|
||||
template<class K>
|
||||
struct object_for
|
||||
{
|
||||
template<class Key, class Value, class Compare, class Allocator>
|
||||
using pair_allocator = typename std::allocator_traits<Allocator>::template rebind_alloc<std::pair<const K, Value>>;
|
||||
|
||||
template<class Key, class Value, class Compare, class Allocator>
|
||||
using type = std::map<K, Value, std::less<K>, pair_allocator<Key, Value, Compare, Allocator>>; // NOLINT(modernize-use-transparent-functors)
|
||||
};
|
||||
|
||||
using json_full = nlohmann::basic_json<object_for<key_full>::type>;
|
||||
using json_no_eq = nlohmann::basic_json<object_for<key_no_eq>::type>;
|
||||
using json_explicit = nlohmann::basic_json<object_for<key_explicit>::type>;
|
||||
using json_to_json = nlohmann::basic_json<object_for<key_to_json>::type>;
|
||||
using json_c_str = nlohmann::basic_json<object_for<key_c_str>::type>;
|
||||
|
||||
// a key that is long enough to need a length byte in CBOR and MessagePack
|
||||
const char* long_key_name(std::size_t i, std::string& storage);
|
||||
const char* long_key_name(std::size_t i, std::string& storage)
|
||||
{
|
||||
storage = "a key longer than thirty-one characters " + std::to_string(i);
|
||||
return storage.c_str();
|
||||
}
|
||||
|
||||
// name of the key at nesting level i of a deep value
|
||||
std::string deep_name(std::size_t i, bool long_keys);
|
||||
std::string deep_name(std::size_t i, bool long_keys)
|
||||
{
|
||||
std::string storage;
|
||||
return (long_keys && i % 2 == 1) ? std::string(long_key_name(i, storage)) : "k" + std::to_string(i);
|
||||
}
|
||||
|
||||
// {"a": 1, "b": [true, null, "x"], "c": {"d": 2.5}, <keys of 23, 36, and 300 characters>}
|
||||
// 23 is the longest CBOR length stored in the initial byte; 36 needs one
|
||||
// length byte in CBOR and MessagePack, 300 needs two
|
||||
template<class J>
|
||||
J make_shallow()
|
||||
{
|
||||
using key_t = typename J::object_t::key_type;
|
||||
|
||||
J array = J::array();
|
||||
array.push_back(J(true));
|
||||
array.push_back(J(nullptr));
|
||||
array.push_back(J("x"));
|
||||
|
||||
typename J::object_t inner;
|
||||
inner.emplace(key_t("d"), J(2.5));
|
||||
|
||||
typename J::object_t object;
|
||||
object.emplace(key_t("a"), J(1));
|
||||
object.emplace(key_t("b"), std::move(array));
|
||||
object.emplace(key_t("c"), J(std::move(inner)));
|
||||
object.emplace(key_t(std::string(23, 'x')), J(2));
|
||||
object.emplace(key_t(std::string(36, 'y')), J(3));
|
||||
object.emplace(key_t(std::string(300, 'z')), J(4));
|
||||
return J(std::move(object));
|
||||
}
|
||||
|
||||
// {"k0": {"k1": {... {"k<depth-1>": 1} ...}}}
|
||||
template<class J>
|
||||
J make_deep(std::size_t depth, bool long_keys)
|
||||
{
|
||||
using key_t = typename J::object_t::key_type;
|
||||
|
||||
J value = 1;
|
||||
for (std::size_t i = depth; i > 0; --i)
|
||||
{
|
||||
typename J::object_t object;
|
||||
object.emplace(key_t(deep_name(i - 1, long_keys)), std::move(value));
|
||||
value = J(std::move(object));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
std::size_t deep_depth();
|
||||
std::size_t deep_depth()
|
||||
{
|
||||
return nlohmann::detail::recursion_depth_limit() + 10;
|
||||
}
|
||||
|
||||
// walk down the nesting levels without recursion and check the leaf
|
||||
template<class J>
|
||||
bool check_deep(const J& value, std::size_t depth, bool long_keys)
|
||||
{
|
||||
using key_t = typename J::object_t::key_type;
|
||||
|
||||
const J* current = &value;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
if (!current->is_object() || current->size() != 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
const auto it = current->find(key_t(deep_name(i, long_keys)));
|
||||
if (it == current->end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
current = &it.value();
|
||||
}
|
||||
return current->is_number_integer() && current->template get<int>() == 1;
|
||||
}
|
||||
|
||||
template<class J>
|
||||
bool check_shallow(const J& value)
|
||||
{
|
||||
using key_t = typename J::object_t::key_type;
|
||||
|
||||
if (!value.is_object() || value.size() != 6)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto a = value.find(key_t("a"));
|
||||
const auto b = value.find(key_t("b"));
|
||||
const auto c = value.find(key_t("c"));
|
||||
if (a == value.end() || b == value.end() || c == value.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto d = c->find(key_t("d"));
|
||||
// basic_json::operator== needs operator== on the keys, which most of the
|
||||
// key types do not have, so the values are checked through get<>()
|
||||
return a->template get<int>() == 1
|
||||
&& b->is_array() && b->size() == 3 && (*b)[0].template get<bool>() && (*b)[1].is_null()
|
||||
&& (*b)[2].template get<std::string>() == "x"
|
||||
&& d != c->end() && d->template get<double>() == 2.5
|
||||
&& value.find(key_t(std::string(23, 'x')))->template get<int>() == 2
|
||||
&& value.find(key_t(std::string(36, 'y')))->template get<int>() == 3
|
||||
&& value.find(key_t(std::string(300, 'z')))->template get<int>() == 4;
|
||||
}
|
||||
|
||||
template<class J>
|
||||
bool is_missing(const J& value, const char* name)
|
||||
{
|
||||
return value.find(typename J::object_t::key_type(name)) == value.end();
|
||||
}
|
||||
|
||||
// member access through find(): at() does not compile for key types without
|
||||
// size() or a conversion to string_t (key_to_json), as in version 3.12.0
|
||||
template<class J>
|
||||
const J& member(const J& value, const char* name)
|
||||
{
|
||||
const auto it = value.find(typename J::object_t::key_type(name));
|
||||
REQUIRE(it != value.end());
|
||||
return *it;
|
||||
}
|
||||
} // namespace custom_key_test
|
||||
|
||||
TEST_CASE_TEMPLATE("custom object key types: copy", J,
|
||||
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
|
||||
custom_key_test::json_to_json, custom_key_test::json_c_str)
|
||||
{
|
||||
SECTION("shallow")
|
||||
{
|
||||
const J original = custom_key_test::make_shallow<J>();
|
||||
REQUIRE(custom_key_test::check_shallow(original));
|
||||
|
||||
const J copy(original); // NOLINT(performance-unnecessary-copy-initialization)
|
||||
CHECK(custom_key_test::check_shallow(copy));
|
||||
|
||||
J assigned;
|
||||
assigned = original;
|
||||
CHECK(custom_key_test::check_shallow(assigned));
|
||||
|
||||
// the original is unchanged
|
||||
CHECK(custom_key_test::check_shallow(original));
|
||||
}
|
||||
|
||||
SECTION("deep")
|
||||
{
|
||||
const std::size_t depth = custom_key_test::deep_depth();
|
||||
|
||||
const J original = custom_key_test::make_deep<J>(depth, false);
|
||||
REQUIRE(custom_key_test::check_deep(original, depth, false));
|
||||
|
||||
const J copy(original); // NOLINT(performance-unnecessary-copy-initialization)
|
||||
CHECK(custom_key_test::check_deep(copy, depth, false));
|
||||
|
||||
J assigned;
|
||||
assigned = original;
|
||||
CHECK(custom_key_test::check_deep(assigned, depth, false));
|
||||
|
||||
CHECK(custom_key_test::check_deep(original, depth, false));
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("custom object key types: parse", J,
|
||||
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
|
||||
custom_key_test::json_to_json, custom_key_test::json_c_str)
|
||||
{
|
||||
const J j = J::parse(R"({"a":1,"b":{"c":[1,2]}})");
|
||||
|
||||
CHECK(j.size() == 2);
|
||||
CHECK(custom_key_test::member(j, "a").template get<int>() == 1);
|
||||
CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c").size() == 2);
|
||||
CHECK(custom_key_test::member(custom_key_test::member(j, "b"), "c")[1].template get<int>() == 2);
|
||||
|
||||
// a deeply nested document
|
||||
const std::size_t depth = custom_key_test::deep_depth();
|
||||
std::string text;
|
||||
for (std::size_t i = 0; i < depth; ++i)
|
||||
{
|
||||
text += "{\"k" + std::to_string(i) + "\":";
|
||||
}
|
||||
text += "1";
|
||||
text.append(depth, '}');
|
||||
CHECK(custom_key_test::check_deep(J::parse(text), depth, false));
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("custom object key types: merge_patch, update, and insert", J,
|
||||
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
|
||||
custom_key_test::json_to_json, custom_key_test::json_c_str)
|
||||
{
|
||||
SECTION("merge_patch")
|
||||
{
|
||||
J j = J::parse(R"({"a":1,"b":2,"n":{"x":1,"y":2}})");
|
||||
j.merge_patch(J::parse(R"({"b":null,"c":3,"n":{"y":null,"z":3}})"));
|
||||
|
||||
CHECK(j.size() == 3);
|
||||
CHECK(custom_key_test::member(j, "a").template get<int>() == 1);
|
||||
CHECK(custom_key_test::is_missing(j, "b"));
|
||||
CHECK(custom_key_test::member(j, "c").template get<int>() == 3);
|
||||
CHECK(custom_key_test::member(j, "n").size() == 2);
|
||||
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get<int>() == 1);
|
||||
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "z").template get<int>() == 3);
|
||||
}
|
||||
|
||||
SECTION("update")
|
||||
{
|
||||
J j = J::parse(R"({"a":1,"b":2,"n":{"x":1}})");
|
||||
const J other = J::parse(R"({"b":3,"c":4,"n":{"y":2}})");
|
||||
|
||||
J replaced = j;
|
||||
replaced.update(other);
|
||||
CHECK(replaced.size() == 4);
|
||||
CHECK(custom_key_test::member(replaced, "a").template get<int>() == 1);
|
||||
CHECK(custom_key_test::member(replaced, "b").template get<int>() == 3);
|
||||
CHECK(custom_key_test::member(replaced, "c").template get<int>() == 4);
|
||||
CHECK(custom_key_test::member(replaced, "n").size() == 1);
|
||||
CHECK(custom_key_test::member(custom_key_test::member(replaced, "n"), "y").template get<int>() == 2);
|
||||
|
||||
j.update(other, true);
|
||||
CHECK(j.size() == 4);
|
||||
CHECK(custom_key_test::member(j, "n").size() == 2);
|
||||
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get<int>() == 1);
|
||||
CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "y").template get<int>() == 2);
|
||||
}
|
||||
|
||||
SECTION("insert")
|
||||
{
|
||||
J j = J::parse(R"({"a":1,"b":2})");
|
||||
const J other = J::parse(R"({"b":3,"c":4})");
|
||||
j.insert(other.begin(), other.end());
|
||||
|
||||
CHECK(j.size() == 3);
|
||||
CHECK(custom_key_test::member(j, "b").template get<int>() == 2);
|
||||
CHECK(custom_key_test::member(j, "c").template get<int>() == 4);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("custom object key types: at() reports a missing key", J,
|
||||
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
|
||||
custom_key_test::json_c_str)
|
||||
{
|
||||
// not for key_to_json: at() needs the key's size() or a conversion to
|
||||
// string_t for its error message, which also was the case in version 3.12.0
|
||||
J j = J::parse(R"({"a":1})");
|
||||
const J& j_const = j;
|
||||
|
||||
CHECK(j.at("a").template get<int>() == 1);
|
||||
CHECK(j_const.at("a").template get<int>() == 1);
|
||||
CHECK_THROWS_WITH_AS(j.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&);
|
||||
CHECK_THROWS_WITH_AS(j_const.at("missing"), "[json.exception.out_of_range.403] key 'missing' not found", typename J::out_of_range&);
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("custom object key types: BSON", J,
|
||||
custom_key_test::json_full, custom_key_test::json_no_eq)
|
||||
{
|
||||
SECTION("shallow")
|
||||
{
|
||||
const J value = custom_key_test::make_shallow<J>();
|
||||
const nlohmann::json expected = custom_key_test::make_shallow<nlohmann::json>();
|
||||
|
||||
const std::vector<std::uint8_t> encoded = J::to_bson(value);
|
||||
CHECK(encoded == nlohmann::json::to_bson(expected));
|
||||
CHECK(nlohmann::json::from_bson(encoded) == expected);
|
||||
}
|
||||
|
||||
SECTION("deep")
|
||||
{
|
||||
const std::size_t depth = custom_key_test::deep_depth();
|
||||
const J value = custom_key_test::make_deep<J>(depth, false);
|
||||
const nlohmann::json expected = custom_key_test::make_deep<nlohmann::json>(depth, false);
|
||||
|
||||
const std::vector<std::uint8_t> encoded = J::to_bson(value);
|
||||
CHECK(encoded == nlohmann::json::to_bson(expected));
|
||||
CHECK(nlohmann::json::from_bson(encoded) == expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("custom object key types: CBOR", J,
|
||||
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
|
||||
custom_key_test::json_to_json, custom_key_test::json_c_str)
|
||||
{
|
||||
SECTION("shallow")
|
||||
{
|
||||
const J value = custom_key_test::make_shallow<J>();
|
||||
const nlohmann::json expected = custom_key_test::make_shallow<nlohmann::json>();
|
||||
|
||||
const std::vector<std::uint8_t> encoded = J::to_cbor(value);
|
||||
CHECK(encoded == nlohmann::json::to_cbor(expected));
|
||||
CHECK(nlohmann::json::from_cbor(encoded) == expected);
|
||||
}
|
||||
|
||||
SECTION("deeper than the recursion depth limit")
|
||||
{
|
||||
const std::size_t depth = custom_key_test::deep_depth();
|
||||
const J value = custom_key_test::make_deep<J>(depth, true);
|
||||
const nlohmann::json expected = custom_key_test::make_deep<nlohmann::json>(depth, true);
|
||||
|
||||
const std::vector<std::uint8_t> encoded = J::to_cbor(value);
|
||||
CHECK(encoded == nlohmann::json::to_cbor(expected));
|
||||
CHECK(nlohmann::json::from_cbor(encoded) == expected);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_CASE_TEMPLATE("custom object key types: MessagePack", J,
|
||||
custom_key_test::json_full, custom_key_test::json_no_eq, custom_key_test::json_explicit,
|
||||
custom_key_test::json_to_json, custom_key_test::json_c_str)
|
||||
{
|
||||
SECTION("shallow")
|
||||
{
|
||||
const J value = custom_key_test::make_shallow<J>();
|
||||
const nlohmann::json expected = custom_key_test::make_shallow<nlohmann::json>();
|
||||
|
||||
const std::vector<std::uint8_t> encoded = J::to_msgpack(value);
|
||||
CHECK(encoded == nlohmann::json::to_msgpack(expected));
|
||||
CHECK(nlohmann::json::from_msgpack(encoded) == expected);
|
||||
}
|
||||
|
||||
SECTION("deeper than the recursion depth limit")
|
||||
{
|
||||
const std::size_t depth = custom_key_test::deep_depth();
|
||||
const J value = custom_key_test::make_deep<J>(depth, true);
|
||||
const nlohmann::json expected = custom_key_test::make_deep<nlohmann::json>(depth, true);
|
||||
|
||||
const std::vector<std::uint8_t> encoded = J::to_msgpack(value);
|
||||
CHECK(encoded == nlohmann::json::to_msgpack(expected));
|
||||
CHECK(nlohmann::json::from_msgpack(encoded) == expected);
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user