diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 855d8af95..c5220e66c 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -166,6 +166,13 @@ jobs: # link, but the binaries clang 11.0.1 and clang 18.1.8 then produce crash # before doctest prints its first line - 39 of 102 tests on clang 18. # Keep the objects small by splitting the test files instead. + # Objects with more than 65535 sections fail to link with "relocation + # truncated to fit: IMAGE_REL_AMD64_REL32 against `.rdata'": the MinGW + # linker stores the section an associative COMDAT section belongs to in + # 16 bits (x_associated in binutils' include/coff/internal.h), so it + # discards the jump tables of inline functions together with the wrong + # function. Each basic_json specialization a test file instantiates adds + # many sections, so test a second one in a file of its own. - name: Run CMake run: cmake -S . -B build ^ -DCMAKE_CXX_COMPILER="C:/Program Files/LLVM/bin/clang++.exe" ^ diff --git a/docs/mkdocs/docs/api/basic_json/binary_t.md b/docs/mkdocs/docs/api/basic_json/binary_t.md index 46b0f7ae2..da13c22a2 100644 --- a/docs/mkdocs/docs/api/basic_json/binary_t.md +++ b/docs/mkdocs/docs/api/basic_json/binary_t.md @@ -64,18 +64,7 @@ values of that type directly to a `basic_json` instance, and they will automatic rather than arrays: ```cpp -using custom_json = nlohmann::basic_json< - nlohmann::ordered_map, // ObjectType - std::vector, // ArrayType - std::string, // StringType - bool, // BooleanType - std::int64_t, // NumberIntegerType - std::uint64_t, // NumberUnsignedType - double, // NumberFloatType - std::allocator, // AllocatorType - nlohmann::adl_serializer, - std::vector // Custom BinaryType ->; +using custom_json = nlohmann::ordered_json::with_binary_t>; std::vector data{std::byte{1}, std::byte{2}, std::byte{3}}; custom_json j = data; // Creates a binary value, not an array diff --git a/docs/mkdocs/docs/examples/as_base_class.cpp b/docs/mkdocs/docs/examples/as_base_class.cpp index 48357b045..b5b62d78f 100644 --- a/docs/mkdocs/docs/examples/as_base_class.cpp +++ b/docs/mkdocs/docs/examples/as_base_class.cpp @@ -15,19 +15,7 @@ class base_class_with_hidden_members } }; -using json = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - base_class_with_hidden_members - >; +using json = nlohmann::json::with_base_class_t; int main() { diff --git a/include/nlohmann/detail/json_pointer.hpp b/include/nlohmann/detail/json_pointer.hpp index 92f52aedc..2ec4e3b58 100644 --- a/include/nlohmann/detail/json_pointer.hpp +++ b/include/nlohmann/detail/json_pointer.hpp @@ -16,8 +16,8 @@ #include // ostream #endif // JSON_NO_IO #include // max +#include // map #include // accumulate -#include // set #include // string #include // move #include // vector @@ -359,33 +359,82 @@ class json_pointer private: /*! - @brief the reference token sequences that denote arrays + @brief the pointer prefixes of a flattened object, and which of them denote arrays @ref unflatten collects the pointer prefixes that have a reference token 0 among their children; @ref get_and_create creates arrays exactly below those prefixes and objects everywhere else. Deciding this up front keeps the result independent of the order in which the flattened object is iterated, which is unspecified for some object types. + + The prefixes form a tree and are numbered, so each of them is stored only + once (as a node) rather than as a copy of all of its reference tokens. */ - using array_parents_t = std::set>; + struct prefix_tree + { + // children[id] maps a reference token to the number of the prefix + // extended by that token; number 0 is the empty prefix + std::vector> children; + // is_array[id] is true iff some flattened key has the reference token + // 0 directly below the prefix with number id + std::vector is_array; + + // start with the empty prefix only + prefix_tree() + : children(1) + , is_array(1, false) + {} + + // return the number of the prefix with number id extended by + // reference_token, adding it if it is new + std::size_t add_child(std::size_t id, string_t&& reference_token) + { + if (reference_token == "0") + { + is_array[id] = true; + } + + // read the number before the emplace_back below, which may + // reallocate children and invalidate the iterator + const std::size_t next = children.size(); + const auto inserted = children[id].emplace(std::move(reference_token), next); + const std::size_t child = inserted.first->second; + if (inserted.second) + { + children.emplace_back(); + is_array.push_back(false); + } + return child; + } + + // return the number of the prefix with number id extended by + // reference_token, which must have been added before + std::size_t find_child(std::size_t id, const string_t& reference_token) const + { + const auto it = children[id].find(reference_token); + JSON_ASSERT(it != children[id].end()); + return it->second; + } + }; /*! @brief create and return a reference to the pointed to value - Complexity: Linear in the number of reference tokens. + Complexity: Linear in the number of reference tokens (times the logarithm + of the number of siblings for the prefix lookup). @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if array index is not a number @throw type_error.313 if value cannot be unflattened */ template - BasicJsonType& get_and_create(BasicJsonType& j, const array_parents_t& array_parents) const + BasicJsonType& get_and_create(BasicJsonType& j, const prefix_tree& tree) const { auto* result = &j; - // the reference tokens that have been consumed so far; used to look up - // whether the value to be created below is an array or an object - std::vector prefix; + // the number of the prefix consumed so far; used to look up whether + // the value to be created below is an array or an object + std::size_t id = 0; // in case no reference tokens exist, return a reference to the JSON value // j which will be overwritten by a primitive value @@ -395,7 +444,7 @@ class json_pointer { case detail::value_t::null: { - if (array_parents.find(prefix) != array_parents.end()) + if (tree.is_array[id]) { // some reference token below this position is 0, so the // value is an array @@ -440,7 +489,7 @@ class json_pointer JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", &j)); } - prefix.push_back(reference_token); + id = tree.find_child(id, reference_token); } return *result; @@ -878,64 +927,131 @@ class json_pointer @param[in,out] result the result object to insert values to @note Empty objects or arrays are flattened to `null`. + + The value is walked with an explicit stack rather than the call stack, so + arbitrarily deeply nested values can be flattened. + + @sa https://github.com/nlohmann/json/issues/5393 */ template static void flatten(const string_t& reference_string, const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + using object_const_iterator = typename BasicJsonType::object_t::const_iterator; + + // an array or object being walked: the container, the array index or + // object iterator of the next child, and the length of the path of the + // container itself + struct frame { - case detail::value_t::array: - { - if (value.m_data.m_value.array->empty()) - { - // flatten empty array as null - result[reference_string] = nullptr; - } - else - { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) - { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); - } - } - break; - } + frame(const BasicJsonType* container_, object_const_iterator member_, const std::size_t path_length_) noexcept + : container(container_), member(std::move(member_)), path_length(path_length_) + {} - case detail::value_t::object: - { - if (value.m_data.m_value.object->empty()) - { - // flatten empty object as null - result[reference_string] = nullptr; - } - else - { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) - { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); - } - } - break; - } + const BasicJsonType* container; + std::size_t index = 0; + object_const_iterator member; + std::size_t path_length; + }; - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: + // The containers being flattened are kept on an explicit stack, and + // every child is flattened completely before the next one, so the + // entries come out in the same order as with a recursive walk. The + // path of the value being flattened is kept in one buffer that grows + // and shrinks with the stack, rather than in a new string per level. + std::vector stack; + string_t path = reference_string; + + // flatten `v`, whose path is `path`: primitives and empty containers + // are added to the result right away; other containers get a frame + const auto enter = [&stack, &path, &result](const BasicJsonType & v) + { + switch (v.type()) { - // add a primitive value with its reference string - result[reference_string] = value; - break; + case detail::value_t::array: + { + if (v.m_data.m_value.array->empty()) + { + // flatten empty array as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, object_const_iterator(), path.size()); + } + return; + } + + case detail::value_t::object: + { + if (v.m_data.m_value.object->empty()) + { + // flatten empty object as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, v.m_data.m_value.object->begin(), path.size()); + } + return; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + { + // add a primitive value with its reference string + result[path] = v; + return; + } + } + }; + + enter(value); + while (!stack.empty()) + { + // the frame is changed through stack.back(): enter() may push a + // frame, which would invalidate a reference to it + const BasicJsonType* const container = stack.back().container; + + // drop the path of the previous child + path.resize(stack.back().path_length); + + if (container->is_array()) + { + const auto& array = *container->m_data.m_value.array; + const std::size_t i = stack.back().index; + if (i == array.size()) + { + stack.pop_back(); + continue; + } + + // iterate array and use index as a reference string + ++stack.back().index; + detail::concat_into(path, '/', detail::to_string(i)); + enter(array[i]); + } + else + { + const object_const_iterator it = stack.back().member; + if (it == container->m_data.m_value.object->end()) + { + stack.pop_back(); + continue; + } + + // iterate object and use keys as reference string + ++stack.back().member; + detail::concat_into(path, '/', detail::escape(it->first)); + enter(it->second); } } } @@ -963,19 +1079,15 @@ class json_pointer // collect the pointer prefixes that have a reference token 0 among // their children; the values below them are arrays, all others are - // objects (see array_parents_t) - array_parents_t array_parents; + // objects (see prefix_tree) + prefix_tree tree; for (const auto& element : *value.m_data.m_value.object) { json_pointer ptr(element.first); - std::vector prefix; + std::size_t id = 0; for (auto& reference_token : ptr.reference_tokens) { - if (reference_token == "0") - { - array_parents.insert(prefix); - } - prefix.push_back(std::move(reference_token)); + id = tree.add_child(id, std::move(reference_token)); } } @@ -991,7 +1103,7 @@ class json_pointer // that if the JSON pointer is "" (i.e., points to the whole value), // function get_and_create returns a reference to the result itself. // An assignment will then create a primitive value. - json_pointer(element.first).get_and_create(result, array_parents) = element.second; + json_pointer(element.first).get_and_create(result, tree) = element.second; } return result; diff --git a/include/nlohmann/detail/output/binary_writer.hpp b/include/nlohmann/detail/output/binary_writer.hpp index f6aa2efb8..e1fa20745 100644 --- a/include/nlohmann/detail/output/binary_writer.hpp +++ b/include/nlohmann/detail/output/binary_writer.hpp @@ -85,6 +85,12 @@ template::value, + const string_t&, string_t >::type; using binary_t = typename BasicJsonType::binary_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -764,8 +770,10 @@ class binary_writer for (const auto& el : *j.m_data.m_value.object) { + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = el.first; string_t storage; - const string_t& key = sanitize_utf8_for_write(el.first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -1305,8 +1313,10 @@ class binary_writer continue; } + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = current.object_it->first; string_t storage; - const string_t& key = sanitize_utf8_for_write(current.object_it->first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -2797,6 +2807,11 @@ class binary_writer itself in every case but a sanitized `replace`/`ignore` one, so @a storage must outlive the returned reference only then. + @a s must be an lvalue that outlives the returned reference. An object key + whose `key_type` is not @ref string_t must therefore first be converted + into a named string_t (see @ref object_key_string_t); the deleted overload + below enforces this at compile time. + @param[in] s the string (value or object key) to write @param[in] context the value @a s belongs to (for diagnostics) @param[out] storage backing storage for a sanitized copy @@ -2826,6 +2841,10 @@ class binary_writer } } + /// deleted: anything but a string_t would bind a temporary that dies before the returned reference is used + template < typename T, enable_if_t < !std::is_same::value, int > = 0 > + const string_t& sanitize_utf8_for_write(const T& /*s*/, const BasicJsonType& /*context*/, string_t& /*storage*/) const = delete; // NOLINT(hicpp-use-equals-delete,modernize-use-equals-delete): a private helper's guard, not part of the interface + /*! @brief write an integer in the shortest encoding diff --git a/include/nlohmann/detail/string_utils.hpp b/include/nlohmann/detail/string_utils.hpp index 4ef748f13..e54a56da6 100644 --- a/include/nlohmann/detail/string_utils.hpp +++ b/include/nlohmann/detail/string_utils.hpp @@ -108,8 +108,8 @@ void encode_utf8(std::uint32_t cp, const Out& out) /////////////////// // UTF-8 decoder states used by decode() below -static constexpr std::uint8_t UTF8_ACCEPT = 0; -static constexpr std::uint8_t UTF8_REJECT = 1; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_ACCEPT = 0; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_REJECT = 1; /*! @brief process a byte of a UTF-8 sequence diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index 5fdbdf331..d5a504619 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -6399,8 +6399,8 @@ void encode_utf8(std::uint32_t cp, const Out& out) /////////////////// // UTF-8 decoder states used by decode() below -static constexpr std::uint8_t UTF8_ACCEPT = 0; -static constexpr std::uint8_t UTF8_REJECT = 1; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_ACCEPT = 0; +JSON_INLINE_VARIABLE constexpr std::uint8_t UTF8_REJECT = 1; /*! @brief process a byte of a UTF-8 sequence @@ -19932,8 +19932,8 @@ NLOHMANN_JSON_NAMESPACE_END #include // ostream #endif // JSON_NO_IO #include // max +#include // map #include // accumulate -#include // set #include // string #include // move #include // vector @@ -20281,33 +20281,82 @@ class json_pointer private: /*! - @brief the reference token sequences that denote arrays + @brief the pointer prefixes of a flattened object, and which of them denote arrays @ref unflatten collects the pointer prefixes that have a reference token 0 among their children; @ref get_and_create creates arrays exactly below those prefixes and objects everywhere else. Deciding this up front keeps the result independent of the order in which the flattened object is iterated, which is unspecified for some object types. + + The prefixes form a tree and are numbered, so each of them is stored only + once (as a node) rather than as a copy of all of its reference tokens. */ - using array_parents_t = std::set>; + struct prefix_tree + { + // children[id] maps a reference token to the number of the prefix + // extended by that token; number 0 is the empty prefix + std::vector> children; + // is_array[id] is true iff some flattened key has the reference token + // 0 directly below the prefix with number id + std::vector is_array; + + // start with the empty prefix only + prefix_tree() + : children(1) + , is_array(1, false) + {} + + // return the number of the prefix with number id extended by + // reference_token, adding it if it is new + std::size_t add_child(std::size_t id, string_t&& reference_token) + { + if (reference_token == "0") + { + is_array[id] = true; + } + + // read the number before the emplace_back below, which may + // reallocate children and invalidate the iterator + const std::size_t next = children.size(); + const auto inserted = children[id].emplace(std::move(reference_token), next); + const std::size_t child = inserted.first->second; + if (inserted.second) + { + children.emplace_back(); + is_array.push_back(false); + } + return child; + } + + // return the number of the prefix with number id extended by + // reference_token, which must have been added before + std::size_t find_child(std::size_t id, const string_t& reference_token) const + { + const auto it = children[id].find(reference_token); + JSON_ASSERT(it != children[id].end()); + return it->second; + } + }; /*! @brief create and return a reference to the pointed to value - Complexity: Linear in the number of reference tokens. + Complexity: Linear in the number of reference tokens (times the logarithm + of the number of siblings for the prefix lookup). @throw parse_error.106 if an array index begins with '0' @throw parse_error.109 if array index is not a number @throw type_error.313 if value cannot be unflattened */ template - BasicJsonType& get_and_create(BasicJsonType& j, const array_parents_t& array_parents) const + BasicJsonType& get_and_create(BasicJsonType& j, const prefix_tree& tree) const { auto* result = &j; - // the reference tokens that have been consumed so far; used to look up - // whether the value to be created below is an array or an object - std::vector prefix; + // the number of the prefix consumed so far; used to look up whether + // the value to be created below is an array or an object + std::size_t id = 0; // in case no reference tokens exist, return a reference to the JSON value // j which will be overwritten by a primitive value @@ -20317,7 +20366,7 @@ class json_pointer { case detail::value_t::null: { - if (array_parents.find(prefix) != array_parents.end()) + if (tree.is_array[id]) { // some reference token below this position is 0, so the // value is an array @@ -20362,7 +20411,7 @@ class json_pointer JSON_THROW(detail::type_error::create(313, "invalid value to unflatten", &j)); } - prefix.push_back(reference_token); + id = tree.find_child(id, reference_token); } return *result; @@ -20800,64 +20849,131 @@ class json_pointer @param[in,out] result the result object to insert values to @note Empty objects or arrays are flattened to `null`. + + The value is walked with an explicit stack rather than the call stack, so + arbitrarily deeply nested values can be flattened. + + @sa https://github.com/nlohmann/json/issues/5393 */ template static void flatten(const string_t& reference_string, const BasicJsonType& value, BasicJsonType& result) { - switch (value.type()) + using object_const_iterator = typename BasicJsonType::object_t::const_iterator; + + // an array or object being walked: the container, the array index or + // object iterator of the next child, and the length of the path of the + // container itself + struct frame { - case detail::value_t::array: - { - if (value.m_data.m_value.array->empty()) - { - // flatten empty array as null - result[reference_string] = nullptr; - } - else - { - // iterate array and use index as a reference string - for (std::size_t i = 0; i < value.m_data.m_value.array->size(); ++i) - { - flatten(detail::concat(reference_string, '/', std::to_string(i)), - value.m_data.m_value.array->operator[](i), result); - } - } - break; - } + frame(const BasicJsonType* container_, object_const_iterator member_, const std::size_t path_length_) noexcept + : container(container_), member(std::move(member_)), path_length(path_length_) + {} - case detail::value_t::object: - { - if (value.m_data.m_value.object->empty()) - { - // flatten empty object as null - result[reference_string] = nullptr; - } - else - { - // iterate object and use keys as reference string - for (const auto& element : *value.m_data.m_value.object) - { - flatten(detail::concat(reference_string, '/', detail::escape(element.first)), element.second, result); - } - } - break; - } + const BasicJsonType* container; + std::size_t index = 0; + object_const_iterator member; + std::size_t path_length; + }; - case detail::value_t::null: - case detail::value_t::string: - case detail::value_t::boolean: - case detail::value_t::number_integer: - case detail::value_t::number_unsigned: - case detail::value_t::number_float: - case detail::value_t::binary: - case detail::value_t::discarded: - default: + // The containers being flattened are kept on an explicit stack, and + // every child is flattened completely before the next one, so the + // entries come out in the same order as with a recursive walk. The + // path of the value being flattened is kept in one buffer that grows + // and shrinks with the stack, rather than in a new string per level. + std::vector stack; + string_t path = reference_string; + + // flatten `v`, whose path is `path`: primitives and empty containers + // are added to the result right away; other containers get a frame + const auto enter = [&stack, &path, &result](const BasicJsonType & v) + { + switch (v.type()) { - // add a primitive value with its reference string - result[reference_string] = value; - break; + case detail::value_t::array: + { + if (v.m_data.m_value.array->empty()) + { + // flatten empty array as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, object_const_iterator(), path.size()); + } + return; + } + + case detail::value_t::object: + { + if (v.m_data.m_value.object->empty()) + { + // flatten empty object as null + result[path] = nullptr; + } + else + { + stack.emplace_back(&v, v.m_data.m_value.object->begin(), path.size()); + } + return; + } + + case detail::value_t::null: + case detail::value_t::string: + case detail::value_t::boolean: + case detail::value_t::number_integer: + case detail::value_t::number_unsigned: + case detail::value_t::number_float: + case detail::value_t::binary: + case detail::value_t::discarded: + default: + { + // add a primitive value with its reference string + result[path] = v; + return; + } + } + }; + + enter(value); + while (!stack.empty()) + { + // the frame is changed through stack.back(): enter() may push a + // frame, which would invalidate a reference to it + const BasicJsonType* const container = stack.back().container; + + // drop the path of the previous child + path.resize(stack.back().path_length); + + if (container->is_array()) + { + const auto& array = *container->m_data.m_value.array; + const std::size_t i = stack.back().index; + if (i == array.size()) + { + stack.pop_back(); + continue; + } + + // iterate array and use index as a reference string + ++stack.back().index; + detail::concat_into(path, '/', detail::to_string(i)); + enter(array[i]); + } + else + { + const object_const_iterator it = stack.back().member; + if (it == container->m_data.m_value.object->end()) + { + stack.pop_back(); + continue; + } + + // iterate object and use keys as reference string + ++stack.back().member; + detail::concat_into(path, '/', detail::escape(it->first)); + enter(it->second); } } } @@ -20885,19 +21001,15 @@ class json_pointer // collect the pointer prefixes that have a reference token 0 among // their children; the values below them are arrays, all others are - // objects (see array_parents_t) - array_parents_t array_parents; + // objects (see prefix_tree) + prefix_tree tree; for (const auto& element : *value.m_data.m_value.object) { json_pointer ptr(element.first); - std::vector prefix; + std::size_t id = 0; for (auto& reference_token : ptr.reference_tokens) { - if (reference_token == "0") - { - array_parents.insert(prefix); - } - prefix.push_back(std::move(reference_token)); + id = tree.add_child(id, std::move(reference_token)); } } @@ -20913,7 +21025,7 @@ class json_pointer // that if the JSON pointer is "" (i.e., points to the whole value), // function get_and_create returns a reference to the result itself. // An assignment will then create a primitive value. - json_pointer(element.first).get_and_create(result, array_parents) = element.second; + json_pointer(element.first).get_and_create(result, tree) = element.second; } return result; @@ -21594,6 +21706,12 @@ template::value, + const string_t&, string_t >::type; using binary_t = typename BasicJsonType::binary_t; using number_float_t = typename BasicJsonType::number_float_t; @@ -22273,8 +22391,10 @@ class binary_writer for (const auto& el : *j.m_data.m_value.object) { + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = el.first; string_t storage; - const string_t& key = sanitize_utf8_for_write(el.first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -22814,8 +22934,10 @@ class binary_writer continue; } + // a converted key must outlive the reference returned by sanitize_utf8_for_write + const object_key_string_t key_string = current.object_it->first; string_t storage; - const string_t& key = sanitize_utf8_for_write(current.object_it->first, j, storage); + const string_t& key = sanitize_utf8_for_write(key_string, j, storage); write_number_with_ubjson_prefix(key.size(), true, use_bjdata); oa.write_characters( reinterpret_cast(key.data()), @@ -24306,6 +24428,11 @@ class binary_writer itself in every case but a sanitized `replace`/`ignore` one, so @a storage must outlive the returned reference only then. + @a s must be an lvalue that outlives the returned reference. An object key + whose `key_type` is not @ref string_t must therefore first be converted + into a named string_t (see @ref object_key_string_t); the deleted overload + below enforces this at compile time. + @param[in] s the string (value or object key) to write @param[in] context the value @a s belongs to (for diagnostics) @param[out] storage backing storage for a sanitized copy @@ -24335,6 +24462,10 @@ class binary_writer } } + /// deleted: anything but a string_t would bind a temporary that dies before the returned reference is used + template < typename T, enable_if_t < !std::is_same::value, int > = 0 > + const string_t& sanitize_utf8_for_write(const T& /*s*/, const BasicJsonType& /*context*/, string_t& /*storage*/) const = delete; // NOLINT(hicpp-use-equals-delete,modernize-use-equals-delete): a private helper's guard, not part of the interface + /*! @brief write an integer in the shortest encoding diff --git a/tests/src/custom_key_test.hpp b/tests/src/custom_key_test.hpp new file mode 100644 index 000000000..28cb31060 --- /dev/null +++ b/tests/src/custom_key_test.hpp @@ -0,0 +1,513 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#pragma once + +#include "doctest_compatibility.h" + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +// 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 +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 +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 +struct object_for +{ + template + using pair_allocator = typename std::allocator_traits::template rebind_alloc>; + + template + using type = std::map, pair_allocator>; // NOLINT(modernize-use-transparent-functors) +}; + +using json_full = nlohmann::json::with_object_t::type>; +using json_no_eq = nlohmann::json::with_object_t::type>; +using json_explicit = nlohmann::json::with_object_t::type>; +using json_to_json = nlohmann::json::with_object_t::type>; +using json_c_str = nlohmann::json::with_object_t::type>; + +// a key that is long enough to need a length byte in CBOR and MessagePack +inline 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 +inline 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}, } +// 23 is the longest CBOR length stored in the initial byte; 36 needs one +// length byte in CBOR and MessagePack, 300 needs two +template +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": 1} ...}}} +template +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; +} + +inline std::size_t deep_depth() +{ + return nlohmann::detail::recursion_depth_limit() + 10; +} + +// walk down the nesting levels without recursion and check the leaf +template +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() == 1; +} + +template +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() == 1 + && b->is_array() && b->size() == 3 && (*b)[0].template get() && (*b)[1].is_null() + && (*b)[2].template get() == "x" + && d != c->end() && std::abs(d->template get() - 2.5) < 1e-9 + && value.find(key_t(std::string(23, 'x')))->template get() == 2 + && value.find(key_t(std::string(36, 'y')))->template get() == 3 + && value.find(key_t(std::string(300, 'z')))->template get() == 4; +} + +template +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 +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; +} + +// The test cases, as function templates: each unit-custom-key-type-*.cpp file +// runs them for one object type only. The MinGW linker fails on objects with +// more than 65535 sections, and every basic_json specialization adds many +// (see .github/workflows/windows.yml). + +// copy +template +void test_copy() +{ + SECTION("shallow") + { + const J original = custom_key_test::make_shallow(); + 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(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)); + } +} + +// parse +template +void test_parse() +{ + 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() == 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() == 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)); +} + +// merge_patch, update, and insert +template +void test_patch() +{ + 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() == 1); + CHECK(custom_key_test::is_missing(j, "b")); + CHECK(custom_key_test::member(j, "c").template get() == 3); + CHECK(custom_key_test::member(j, "n").size() == 2); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "z").template get() == 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() == 1); + CHECK(custom_key_test::member(replaced, "b").template get() == 3); + CHECK(custom_key_test::member(replaced, "c").template get() == 4); + CHECK(custom_key_test::member(replaced, "n").size() == 1); + CHECK(custom_key_test::member(custom_key_test::member(replaced, "n"), "y").template get() == 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() == 1); + CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "y").template get() == 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() == 2); + CHECK(custom_key_test::member(j, "c").template get() == 4); + } +} + +// at() reports a missing key +template +void test_at() +{ + // 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() == 1); + CHECK(j_const.at("a").template get() == 1); +#if JSON_DIAGNOSTIC_POSITIONS + CHECK_THROWS_WITH_AS(j.at("missing"), "[json.exception.out_of_range.403] (bytes 0-7) key 'missing' not found", typename J::out_of_range&); + CHECK_THROWS_WITH_AS(j_const.at("missing"), "[json.exception.out_of_range.403] (bytes 0-7) key 'missing' not found", typename J::out_of_range&); +#else + 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&); +#endif +} + +// BSON +template +void test_bson() +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector 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(depth, false); + const nlohmann::json expected = custom_key_test::make_deep(depth, false); + + const std::vector encoded = J::to_bson(value); + CHECK(encoded == nlohmann::json::to_bson(expected)); + CHECK(nlohmann::json::from_bson(encoded) == expected); + } +} + +// CBOR +template +void test_cbor() +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector 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(depth, true); + const nlohmann::json expected = custom_key_test::make_deep(depth, true); + + const std::vector encoded = J::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); + } +} + +// MessagePack +template +void test_msgpack() +{ + SECTION("shallow") + { + const J value = custom_key_test::make_shallow(); + const nlohmann::json expected = custom_key_test::make_shallow(); + + const std::vector 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(depth, true); + const nlohmann::json expected = custom_key_test::make_deep(depth, true); + + const std::vector encoded = J::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); + } +} + +} // namespace custom_key_test diff --git a/tests/src/unit-binary_utf8_error_handler.cpp b/tests/src/unit-binary_utf8_error_handler.cpp index c3f2435fd..114c92885 100644 --- a/tests/src/unit-binary_utf8_error_handler.cpp +++ b/tests/src/unit-binary_utf8_error_handler.cpp @@ -12,7 +12,11 @@ #include using nlohmann::json; +#include +#include +#include #include +#include #include namespace @@ -55,6 +59,53 @@ std::string dump_and_parse(const std::string& raw, eh error_handler) return json::parse(json(raw).dump(-1, ' ', false, error_handler)).get(); } +// an object key type that is not string_t, but converts implicitly to it; +// data() is only used when JSON_DIAGNOSTICS is enabled +DOCTEST_CLANG_SUPPRESS_WARNING_PUSH +DOCTEST_CLANG_SUPPRESS_WARNING("-Wunused-member-function") +class converting_key +{ + public: + converting_key(const char* s) : m_value(s) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + converting_key(std::string s) : m_value(std::move(s)) {} // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + + // the conversion yields a temporary string_t + operator std::string() const // NOLINT(google-explicit-constructor,hicpp-explicit-conversions) + { + return m_value; + } + + // read by the exception messages when JSON_DIAGNOSTICS is enabled + const char* data() const noexcept + { + return m_value.data(); + } + + friend bool operator<(const converting_key& lhs, const converting_key& rhs) + { + return lhs.m_value < rhs.m_value; + } + + private: + std::string m_value; +}; +DOCTEST_CLANG_SUPPRESS_WARNING_POP + +// ObjectType using converting_key; the Key template argument is ignored +template +class converting_key_object : public std::map, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc>> +{ + using base_type = std::map, // NOLINT(modernize-use-transparent-functors) + typename std::allocator_traits::template rebind_alloc>>; + + public: + using base_type::base_type; + using base_type::operator=; +}; + +using converting_key_json = nlohmann::basic_json; + } // namespace TEST_CASE("UTF-8 error_handler for the binary readers and writers") @@ -370,3 +421,109 @@ TEST_CASE("UTF-8 error_handler for the binary readers and writers") CHECK(json::from_bson(bson_bytes)["k"].get() == ill_formed_cases()[0].bytes); } } + +// The UBJSON and BJData writers bind the (possibly sanitized) key to a const +// string_t&. If key_type is not string_t but converts to it, the converted +// temporary must outlive that reference; this was a use-after-scope found by +// AddressSanitizer. Keys exceed the small string optimization on purpose. +TEST_CASE("UBJSON and BJData writers with an object_t whose key_type is not string_t") +{ + const std::string long_prefix(70, 'k'); + + SECTION("well-formed keys, every error_handler") + { + const std::string key1 = long_prefix + "-first"; + const std::string key2 = long_prefix + "-second"; + + converting_key_json::object_t o; + o.emplace(converting_key(key1), 1); + o.emplace(converting_key(key2), "value"); + const converting_key_json v(std::move(o)); + + json expected; + expected[key1] = 1; + expected[key2] = "value"; + + const std::array, 3> combos = {{{false, false}, {true, false}, {true, true}}}; + for (const auto h : all_handlers()) + { + CAPTURE(static_cast(h)) + for (const auto& combo : combos) + { + const bool use_count = combo.first; + const bool use_type = combo.second; + CAPTURE(use_count) + CAPTURE(use_type) + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, use_count, use_type, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, use_type, json::bjdata_version_t::draft2, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, use_type, json::bjdata_version_t::draft3, h)) == expected); + } + } + } + + SECTION("ill-formed keys") + { + for (const auto& c : ill_formed_cases()) + { + CAPTURE(c.name) + const std::string key = long_prefix + c.bytes; + + converting_key_json::object_t o; + o.emplace(converting_key(key), 1); + const converting_key_json v(std::move(o)); + + CHECK_THROWS_AS(converting_key_json::to_ubjson(v, false, false, eh::strict), converting_key_json::type_error&); + CHECK_THROWS_AS(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, eh::strict), converting_key_json::type_error&); + + for (const auto h : + { + eh::replace, eh::ignore + }) + { + CAPTURE(static_cast(h)) + const std::string expected = dump_and_parse(key, h); + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, false, false, h)).begin().key() == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, h)).begin().key() == expected); + } + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, false, false, eh::keep)).begin().key() == key); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, false, false, json::bjdata_version_t::draft2, eh::keep)).begin().key() == key); + } + } + + SECTION("nested deeper than the recursion limit") + { + // wrap the previous value, innermost first + converting_key_json v = 42; + json expected = 42; + for (int i = 199; i >= 0; --i) + { + const std::string key = "level-" + std::to_string(i) + "-" + std::string(64, 'x'); + + converting_key_json::object_t o; + o.emplace(converting_key(key), std::move(v)); + v = converting_key_json(std::move(o)); + + json e; + e[key] = std::move(expected); + expected = std::move(e); + } + + for (const auto h : all_handlers()) + { + CAPTURE(static_cast(h)) + for (const bool use_count : + { + false, true + }) + { + CAPTURE(use_count) + + CHECK(json::from_ubjson(converting_key_json::to_ubjson(v, use_count, false, h)) == expected); + CHECK(json::from_bjdata(converting_key_json::to_bjdata(v, use_count, false, json::bjdata_version_t::draft2, h)) == expected); + } + } + } +} diff --git a/tests/src/unit-bson.cpp b/tests/src/unit-bson.cpp index ea5ca3130..03c901cee 100644 --- a/tests/src/unit-bson.cpp +++ b/tests/src/unit-bson.cpp @@ -46,9 +46,7 @@ class huge_binary_t : public std::vector } }; -using huge_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, huge_binary_t, void >; +using huge_binary_json = nlohmann::json::with_binary_t; // a string type that can be made to report a size beyond INT32_MAX without // allocating that much memory, so BSON length overflow can be tested for @@ -96,9 +94,7 @@ class huge_string_t : public std::string bool pretend_huge = false; }; -using huge_string_json = nlohmann::basic_json < - std::map, std::vector, huge_string_t, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +using huge_string_json = nlohmann::json::with_string_t; } // namespace TEST_CASE("BSON") diff --git a/tests/src/unit-cbor.cpp b/tests/src/unit-cbor.cpp index 9b57d0664..9ee371495 100644 --- a/tests/src/unit-cbor.cpp +++ b/tests/src/unit-cbor.cpp @@ -28,7 +28,6 @@ using nlohmann::json; #include "make_test_data_available.hpp" #include "round_trip_corpus.hpp" #include "test_utils.hpp" -#include "custom_object_key_type.hpp" #include "sax_countdown.hpp" using utils::SaxCountdown; @@ -3358,55 +3357,3 @@ TEST_CASE("CBOR large strings and binaries (chunked reader)") } } } - -TEST_CASE("CBOR supports custom object key types") -{ - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - custom_json::object_t object; - object.emplace(custom_key{"short"}, 1); - object.emplace( - custom_key{"a key longer than twenty-three characters"}, - 2); - - const custom_json value(std::move(object)); - const auto encoded = custom_json::to_cbor(value); - - CHECK(nlohmann::json::from_cbor(encoded) == nlohmann::json - { - {"short", 1}, - {"a key longer than twenty-three characters", 2} - }); -} - -TEST_CASE("CBOR supports custom object key types nested deeper than the recursion depth limit") -{ - // below detail::recursion_depth_limit(), keys are written by - // write_cbor_iterative instead of write_cbor - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; - - custom_json value = 1; - nlohmann::json expected = 1; - for (std::size_t i = 0; i < depth; ++i) - { - // alternate short keys with ones long enough to need a length byte - const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) - : "a key longer than thirty-one characters " + std::to_string(i); - - custom_json::object_t object; - object.emplace(custom_key{name}, std::move(value)); - value = custom_json(std::move(object)); - - nlohmann::json::object_t expected_object; - expected_object.emplace(name, std::move(expected)); - expected = nlohmann::json(std::move(expected_object)); - } - - const auto encoded = custom_json::to_cbor(value); - CHECK(encoded == nlohmann::json::to_cbor(expected)); - CHECK(nlohmann::json::from_cbor(encoded) == expected); -} diff --git a/tests/src/unit-custom-base-class.cpp b/tests/src/unit-custom-base-class.cpp index 9718d1cda..673c7ddbb 100644 --- a/tests/src/unit-custom-base-class.cpp +++ b/tests/src/unit-custom-base-class.cpp @@ -400,20 +400,7 @@ class base_class_with_hidden_members std::size_t m_size = 42; }; -using json_with_hidden_base_members = - nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - base_class_with_hidden_members - >; +using json_with_hidden_base_members = nlohmann::json::with_base_class_t; TEST_CASE("JSON Node as_base_class") { @@ -459,19 +446,7 @@ struct const_member_base const int id = 7; // NOLINT(misc-non-private-member-variables-in-classes) }; -using json_with_const_base = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - const_member_base - >; +using json_with_const_base = nlohmann::json::with_base_class_t; // build an array nested @a depth levels deep, with the innermost value 1; // every level is constructed (never assigned), since const_member_base does diff --git a/tests/src/unit-custom-binary-type.cpp b/tests/src/unit-custom-binary-type.cpp index d885376cb..6b504bd51 100644 --- a/tests/src/unit-custom-binary-type.cpp +++ b/tests/src/unit-custom-binary-type.cpp @@ -26,15 +26,11 @@ namespace // a BinaryType whose value type is signed: the elements must still be // processed as the numbers 0..255 -using char_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +using char_binary_json = nlohmann::json::with_binary_t>; #ifdef JSON_HAS_CPP_17 // a BinaryType whose value type is not an integer type at all - using byte_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; + using byte_binary_json = nlohmann::json::with_binary_t>; #endif } // namespace diff --git a/tests/src/unit-custom-key-type-c-str.cpp b/tests/src/unit-custom-key-type-c-str.cpp new file mode 100644 index 000000000..5ad1edc90 --- /dev/null +++ b/tests/src/unit-custom-key-type-c-str.cpp @@ -0,0 +1,46 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has no conversion, but size() and c_str() (see custom_key_test.hpp) +using custom_key_test::json_c_str; + +TEST_CASE("custom object key types: copy (json_c_str)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_c_str)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_c_str)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_c_str)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: CBOR (json_c_str)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_c_str)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-explicit.cpp b/tests/src/unit-custom-key-type-explicit.cpp new file mode 100644 index 000000000..2ef3e97f1 --- /dev/null +++ b/tests/src/unit-custom-key-type-explicit.cpp @@ -0,0 +1,46 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has explicit conversion to std::string, no operator== (see custom_key_test.hpp) +using custom_key_test::json_explicit; + +TEST_CASE("custom object key types: copy (json_explicit)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_explicit)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_explicit)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_explicit)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: CBOR (json_explicit)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_explicit)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-full.cpp b/tests/src/unit-custom-key-type-full.cpp new file mode 100644 index 000000000..b72bd7ae0 --- /dev/null +++ b/tests/src/unit-custom-key-type-full.cpp @@ -0,0 +1,51 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has implicit conversion to std::string and operator== (see custom_key_test.hpp) +using custom_key_test::json_full; + +TEST_CASE("custom object key types: copy (json_full)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_full)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_full)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_full)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: BSON (json_full)") +{ + custom_key_test::test_bson(); +} + +TEST_CASE("custom object key types: CBOR (json_full)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_full)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-no-eq.cpp b/tests/src/unit-custom-key-type-no-eq.cpp new file mode 100644 index 000000000..288b9fdd2 --- /dev/null +++ b/tests/src/unit-custom-key-type-no-eq.cpp @@ -0,0 +1,51 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has implicit conversion to std::string, no operator== (see custom_key_test.hpp) +using custom_key_test::json_no_eq; + +TEST_CASE("custom object key types: copy (json_no_eq)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_no_eq)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_no_eq)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: at() reports a missing key (json_no_eq)") +{ + custom_key_test::test_at(); +} + +TEST_CASE("custom object key types: BSON (json_no_eq)") +{ + custom_key_test::test_bson(); +} + +TEST_CASE("custom object key types: CBOR (json_no_eq)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_no_eq)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-key-type-to-json.cpp b/tests/src/unit-custom-key-type-to-json.cpp new file mode 100644 index 000000000..fde2a8ad3 --- /dev/null +++ b/tests/src/unit-custom-key-type-to-json.cpp @@ -0,0 +1,41 @@ +// __ _____ _____ _____ +// __| | __| | | | JSON for Modern C++ (supporting code) +// | | |__ | | | | | | version 3.12.0 +// |_____|_____|_____|_|___| https://github.com/nlohmann/json +// +// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann +// SPDX-License-Identifier: MIT + +#include "doctest_compatibility.h" + +#include + +#include "custom_key_test.hpp" + +// object type whose key type has no conversion, only a to_json overload (see custom_key_test.hpp) +using custom_key_test::json_to_json; + +TEST_CASE("custom object key types: copy (json_to_json)") +{ + custom_key_test::test_copy(); +} + +TEST_CASE("custom object key types: parse (json_to_json)") +{ + custom_key_test::test_parse(); +} + +TEST_CASE("custom object key types: merge_patch, update, and insert (json_to_json)") +{ + custom_key_test::test_patch(); +} + +TEST_CASE("custom object key types: CBOR (json_to_json)") +{ + custom_key_test::test_cbor(); +} + +TEST_CASE("custom object key types: MessagePack (json_to_json)") +{ + custom_key_test::test_msgpack(); +} diff --git a/tests/src/unit-custom-object-key-type.cpp b/tests/src/unit-custom-object-key-type.cpp index 40de28ed2..9c64ebce0 100644 --- a/tests/src/unit-custom-object-key-type.cpp +++ b/tests/src/unit-custom-object-key-type.cpp @@ -10,493 +10,118 @@ #include -#include #include -#include -#include -#include #include #include -#include -// 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. +#include "custom_object_key_type.hpp" -namespace custom_key_test +// These tests instantiate a second basic_json specialization. They live in +// their own file rather than in unit-cbor.cpp and unit-msgpack.cpp to keep +// those objects below 65535 sections: the MinGW linker stores the section a +// COMDAT section is associated with in 16 bits, so it misplaces the jump +// tables of larger objects (see the clang job in windows.yml). + +TEST_CASE("CBOR supports custom object key types") { -class key_base -{ - public: - key_base() = default; + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; - key_base(const char* value) - : m_value(value) - {} + custom_json::object_t object; + object.emplace(custom_key{"short"}, 1); + object.emplace( + custom_key{"a key longer than twenty-three characters"}, + 2); - key_base(std::string value) - : m_value(std::move(value)) - {} + const custom_json value(std::move(object)); + const auto encoded = custom_json::to_cbor(value); - // Required by JSON_DIAGNOSTICS, which reads object keys through data() - // when building the path of an exception. - const char* data() const noexcept + CHECK(nlohmann::json::from_cbor(encoded) == nlohmann::json { - 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 -void to_json(BasicJsonType& j, const key_to_json& k) -{ - j = k.value(); + {"short", 1}, + {"a key longer than twenty-three characters", 2} + }); } -// like key_to_json, but with size() and c_str() -class key_c_str : public key_base +TEST_CASE("CBOR supports custom object key types nested deeper than the recursion depth limit") { - public: - key_c_str() = default; - using key_base::key_base; + // below detail::recursion_depth_limit(), keys are written by + // write_cbor_iterative instead of write_cbor + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; - const std::string& value() const - { - return m_value; - } + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; - std::size_t size() const - { - return m_value.size(); - } - - const char* c_str() const - { - return m_value.c_str(); - } -}; - -template -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 -struct object_for -{ - template - using pair_allocator = typename std::allocator_traits::template rebind_alloc>; - - template - using type = std::map, pair_allocator>; // NOLINT(modernize-use-transparent-functors) -}; - -using json_full = nlohmann::json::with_object_t::type>; -using json_no_eq = nlohmann::json::with_object_t::type>; -using json_explicit = nlohmann::json::with_object_t::type>; -using json_to_json = nlohmann::json::with_object_t::type>; -using json_c_str = nlohmann::json::with_object_t::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}, } -// 23 is the longest CBOR length stored in the initial byte; 36 needs one -// length byte in CBOR and MessagePack, 300 needs two -template -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": 1} ...}}} -template -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 -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; + custom_json value = 1; + nlohmann::json expected = 1; 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(); + // alternate short keys with ones long enough to need a length byte + const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) + : "a key longer than thirty-one characters " + std::to_string(i); + + custom_json::object_t object; + object.emplace(custom_key{name}, std::move(value)); + value = custom_json(std::move(object)); + + nlohmann::json::object_t expected_object; + expected_object.emplace(name, std::move(expected)); + expected = nlohmann::json(std::move(expected_object)); } - return current->is_number_integer() && current->template get() == 1; + + const auto encoded = custom_json::to_cbor(value); + CHECK(encoded == nlohmann::json::to_cbor(expected)); + CHECK(nlohmann::json::from_cbor(encoded) == expected); } -template -bool check_shallow(const J& value) +TEST_CASE("MessagePack supports custom object key types") { - using key_t = typename J::object_t::key_type; + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; - if (!value.is_object() || value.size() != 6) + custom_json::object_t object; + object.emplace(custom_key{"short"}, 1); + object.emplace( + custom_key{"a key longer than thirty-one characters"}, + 2); + + const custom_json value(std::move(object)); + const auto encoded = custom_json::to_msgpack(value); + + CHECK(nlohmann::json::from_msgpack(encoded) == nlohmann::json { - 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() == 1 - && b->is_array() && b->size() == 3 && (*b)[0].template get() && (*b)[1].is_null() - && (*b)[2].template get() == "x" - && d != c->end() && std::abs(d->template get() - 2.5) < 1e-9 - && value.find(key_t(std::string(23, 'x')))->template get() == 2 - && value.find(key_t(std::string(36, 'y')))->template get() == 3 - && value.find(key_t(std::string(300, 'z')))->template get() == 4; + {"short", 1}, + {"a key longer than thirty-one characters", 2} + }); } -template -bool is_missing(const J& value, const char* name) +TEST_CASE("MessagePack supports custom object key types nested deeper than the recursion depth limit") { - return value.find(typename J::object_t::key_type(name)) == value.end(); -} + // below detail::recursion_depth_limit(), keys are written by + // write_msgpack_iterative instead of write_msgpack + using custom_json = custom_object_key_test::json; + using custom_key = custom_object_key_test::key; -// 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 -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 + const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; -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(); - 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(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() == 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() == 2); - - // a deeply nested document - const std::size_t depth = custom_key_test::deep_depth(); - std::string text; + custom_json value = 1; + nlohmann::json expected = 1; 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)); -} + // alternate short keys with ones long enough to need a length byte + const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) + : "a key longer than thirty-one characters " + std::to_string(i); -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}})")); + custom_json::object_t object; + object.emplace(custom_key{name}, std::move(value)); + value = custom_json(std::move(object)); - CHECK(j.size() == 3); - CHECK(custom_key_test::member(j, "a").template get() == 1); - CHECK(custom_key_test::is_missing(j, "b")); - CHECK(custom_key_test::member(j, "c").template get() == 3); - CHECK(custom_key_test::member(j, "n").size() == 2); - CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "x").template get() == 1); - CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "z").template get() == 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() == 1); - CHECK(custom_key_test::member(replaced, "b").template get() == 3); - CHECK(custom_key_test::member(replaced, "c").template get() == 4); - CHECK(custom_key_test::member(replaced, "n").size() == 1); - CHECK(custom_key_test::member(custom_key_test::member(replaced, "n"), "y").template get() == 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() == 1); - CHECK(custom_key_test::member(custom_key_test::member(j, "n"), "y").template get() == 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() == 2); - CHECK(custom_key_test::member(j, "c").template get() == 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() == 1); - CHECK(j_const.at("a").template get() == 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(); - const nlohmann::json expected = custom_key_test::make_shallow(); - - const std::vector 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(depth, false); - const nlohmann::json expected = custom_key_test::make_deep(depth, false); - - const std::vector 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(); - const nlohmann::json expected = custom_key_test::make_shallow(); - - const std::vector 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(depth, true); - const nlohmann::json expected = custom_key_test::make_deep(depth, true); - - const std::vector 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(); - const nlohmann::json expected = custom_key_test::make_shallow(); - - const std::vector 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(depth, true); - const nlohmann::json expected = custom_key_test::make_deep(depth, true); - - const std::vector encoded = J::to_msgpack(value); - CHECK(encoded == nlohmann::json::to_msgpack(expected)); - CHECK(nlohmann::json::from_msgpack(encoded) == expected); + nlohmann::json::object_t expected_object; + expected_object.emplace(name, std::move(expected)); + expected = nlohmann::json(std::move(expected_object)); } + + const auto encoded = custom_json::to_msgpack(value); + CHECK(encoded == nlohmann::json::to_msgpack(expected)); + CHECK(nlohmann::json::from_msgpack(encoded) == expected); } diff --git a/tests/src/unit-json_pointer.cpp b/tests/src/unit-json_pointer.cpp index b4ed2c9cc..1faf511f4 100644 --- a/tests/src/unit-json_pointer.cpp +++ b/tests/src/unit-json_pointer.cpp @@ -939,3 +939,114 @@ TEST_CASE("unescaping keeps a '~' that does not start an escape sequence") nlohmann::detail::unescape(s); CHECK(s == "~/~"); } + +TEST_CASE("flatten of structured values") +{ + SECTION("values nested too deeply for the call stack (#5393)") + { + // flatten() used to recurse once per nesting level + const std::size_t depth = 100000; + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects) + std::string text; + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + text += objects ? "{\"a\":" : "["; + path += objects ? "/a" : "/0"; + } + text += "0"; + text += std::string(depth, objects ? '}' : ']'); + const auto value = json::parse(text); + + const auto flat = value.flatten(); + REQUIRE(flat.size() == 1); + REQUIRE(flat.begin().key().size() == path.size()); + CHECK(flat.begin().key() == path); + CHECK(flat.begin().value() == 0); + + // unflatten() is linear in the depth, so the value roundtrips + CHECK(flat.unflatten() == value); + } + } + + SECTION("unflatten of a deeply nested pointer") + { + const std::size_t depth = 100000; + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects) + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + path += objects ? "/a" : "/0"; + } + + json flat = json::object(); + flat[path] = 1; + const json value = flat.unflatten(); + + // walk down iteratively + std::size_t levels = 0; + const json* current = &value; + while (objects ? current->is_object() : current->is_array()) + { + REQUIRE(current->size() == 1); + current = objects ? ¤t->at("a") : ¤t->at(0); + ++levels; + } + CHECK(levels == depth); + CHECK(*current == 1); + } + } + + SECTION("unflatten does not depend on the iteration order") + { + // the "0" key comes after its sibling in iteration order + const nlohmann::ordered_json flat_array = nlohmann::ordered_json::parse(R"({"/a/1": 2, "/a/0": 1})"); + CHECK(flat_array.unflatten() == nlohmann::ordered_json::parse(R"({"a": [1, 2]})")); + + const nlohmann::ordered_json flat_object = nlohmann::ordered_json::parse(R"({"/b/1": 2})"); + CHECK(flat_object.unflatten() == nlohmann::ordered_json::parse(R"({"b": {"1": 2}})")); + } + + SECTION("objects and arrays interleaved") + { + const json value = + { + {"a", {1, {{"b", json::array()}, {"c", json::object()}}, json::array({{{"x~/", {true, nullptr}}}})}}, + {"a/b", {{"~", 1}}}, + {"z", "s"} + }; + + const json expected = + { + {"/a/0", 1}, + {"/a/1/b", nullptr}, + {"/a/1/c", nullptr}, + {"/a/2/0/x~0~1/0", true}, + {"/a/2/0/x~0~1/1", nullptr}, + {"/a~1b/~0", 1}, + {"/z", "s"} + }; + + CHECK(value.flatten() == expected); + } + + SECTION("order of the entries of an ordered_json") + { + const auto value = nlohmann::ordered_json::parse( + R"({"z":"s","a/b":{"~":1,"k":[]},"a":[1,{"c":{},"b":[]},[{"x~/":[true,null],"w":2}]]})"); + + const auto flat = value.flatten(); + CHECK(flat.dump() == + R"({"/z":"s","/a~1b/~0":1,"/a~1b/k":null,"/a/0":1,"/a/1/c":null,"/a/1/b":null,"/a/2/0/x~0~1/0":true,"/a/2/0/x~0~1/1":null,"/a/2/0/w":2})"); + } +} diff --git a/tests/src/unit-msgpack.cpp b/tests/src/unit-msgpack.cpp index 0e07c8ea4..aeb0116d3 100644 --- a/tests/src/unit-msgpack.cpp +++ b/tests/src/unit-msgpack.cpp @@ -31,7 +31,6 @@ using nlohmann::json; #include "make_test_data_available.hpp" #include "round_trip_corpus.hpp" #include "test_utils.hpp" -#include "custom_object_key_type.hpp" #include "sax_countdown.hpp" using utils::SaxCountdown; @@ -2202,10 +2201,7 @@ struct huge_array : std::vector } }; -using huge_array_json = nlohmann::basic_json < - std::map, huge_array, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, - std::vector, void >; +using huge_array_json = nlohmann::json::with_array_t; TEST_CASE("MessagePack Size above uint32 for array") { @@ -2250,18 +2246,7 @@ template, - void >; +using huge_object_json = nlohmann::json::with_object_t; TEST_CASE("MessagePack Size above uint32 for object") { @@ -2296,18 +2281,7 @@ struct huge_string : std::string } }; -using huge_string_json = nlohmann::basic_json < - std::map, - std::vector, - huge_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - std::vector, - void >; +using huge_string_json = nlohmann::json::with_string_t; TEST_CASE("MessagePack Size above uint32 for string") { @@ -2330,18 +2304,7 @@ struct huge_binary : std::vector } }; -using huge_binary_json = nlohmann::basic_json < - std::map, - std::vector, - std::string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer, - huge_binary, - void >; +using huge_binary_json = nlohmann::json::with_binary_t; TEST_CASE("MessagePack Size above uint32 for binary") { @@ -2391,14 +2354,10 @@ class beyond_uint32_string_t : public std::string } }; -using beyond_uint32_string_json = nlohmann::basic_json < - std::map, std::vector, beyond_uint32_string_t, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, std::vector, void >; +using beyond_uint32_string_json = nlohmann::json::with_string_t; #endif -using beyond_uint32_binary_json = nlohmann::basic_json < - std::map, std::vector, std::string, bool, std::int64_t, std::uint64_t, - double, std::allocator, nlohmann::adl_serializer, beyond_uint32_binary_t, void >; +using beyond_uint32_binary_json = nlohmann::json::with_binary_t; } // namespace TEST_CASE("MessagePack lengths beyond UINT32_MAX cannot be serialized") @@ -2523,55 +2482,3 @@ TEST_CASE("MessagePack large strings and binaries (chunked reader)") } } } - -TEST_CASE("MessagePack supports custom object key types") -{ - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - custom_json::object_t object; - object.emplace(custom_key{"short"}, 1); - object.emplace( - custom_key{"a key longer than thirty-one characters"}, - 2); - - const custom_json value(std::move(object)); - const auto encoded = custom_json::to_msgpack(value); - - CHECK(nlohmann::json::from_msgpack(encoded) == nlohmann::json - { - {"short", 1}, - {"a key longer than thirty-one characters", 2} - }); -} - -TEST_CASE("MessagePack supports custom object key types nested deeper than the recursion depth limit") -{ - // below detail::recursion_depth_limit(), keys are written by - // write_msgpack_iterative instead of write_msgpack - using custom_json = custom_object_key_test::json; - using custom_key = custom_object_key_test::key; - - const std::size_t depth = nlohmann::detail::recursion_depth_limit() + 10; - - custom_json value = 1; - nlohmann::json expected = 1; - for (std::size_t i = 0; i < depth; ++i) - { - // alternate short keys with ones long enough to need a length byte - const std::string name = (i % 2 == 0) ? "k" + std::to_string(i) - : "a key longer than thirty-one characters " + std::to_string(i); - - custom_json::object_t object; - object.emplace(custom_key{name}, std::move(value)); - value = custom_json(std::move(object)); - - nlohmann::json::object_t expected_object; - expected_object.emplace(name, std::move(expected)); - expected = nlohmann::json(std::move(expected_object)); - } - - const auto encoded = custom_json::to_msgpack(value); - CHECK(encoded == nlohmann::json::to_msgpack(expected)); - CHECK(nlohmann::json::from_msgpack(encoded) == expected); -} diff --git a/tests/src/unit-ordered_json2.cpp b/tests/src/unit-ordered_json2.cpp index 653820628..4f4e18844 100644 --- a/tests/src/unit-ordered_json2.cpp +++ b/tests/src/unit-ordered_json2.cpp @@ -217,16 +217,7 @@ void int_to_string(alt_string& target, std::size_t value) target = std::to_string(value).c_str(); } -using alt_json = nlohmann::basic_json < - std::map, - std::vector, - alt_string, - bool, - std::int64_t, - std::uint64_t, - double, - std::allocator, - nlohmann::adl_serializer >; +using alt_json = nlohmann::json::with_string_t; bool operator<(const char* op1, const alt_string& op2) noexcept {