diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index bc667b86d..21e48a150 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -5381,6 +5381,73 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT static basic_json diff(const basic_json& source, const basic_json& target, const string_t& path = "") + { + // Diffing descends into both values once per nesting level and + // compares them with operator== on the way, which recurses as well, + // so values nested deeply enough used to exhaust the call stack. + // Both only descend as far as the source is nested, so a source + // nested no more than diff_depth_limit() levels deep - all but a + // vanishing minority - is diffed recursively as before; deeper ones + // are diffed without the call stack. + if (JSON_HEDLEY_LIKELY(!nesting_exceeds(source, diff_depth_limit()))) + { + return diff_recursively(source, target, path); + } + return diff_iteratively(source, target, path); + } + + JSON_PRIVATE_UNLESS_TESTED: + /// the nesting depth up to which @ref diff recurses + static constexpr std::size_t diff_depth_limit() noexcept + { + return 128; + } + + private: + /*! + @brief whether @a j is nested more than @a limit levels deep + + A primitive value is not nested at all, an array or object one level more + than its most deeply nested element. Recurses at most @a limit levels. + */ + static bool nesting_exceeds(const basic_json& j, const std::size_t limit) + { + switch (j.m_data.m_type) + { + case value_t::array: + { + return limit == 0 || std::any_of(j.m_data.m_value.array->cbegin(), j.m_data.m_value.array->cend(), + [limit](const basic_json & element) + { + return element.is_structured() && nesting_exceeds(element, limit - 1); + }); + } + + case value_t::object: + { + return limit == 0 || std::any_of(j.m_data.m_value.object->cbegin(), j.m_data.m_value.object->cend(), + [limit](const typename object_t::value_type & element) + { + return element.second.is_structured() && nesting_exceeds(element.second, limit - 1); + }); + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return false; + } + } + + /// @ref diff for a @a source nested no more than @ref diff_depth_limit levels deep + static basic_json diff_recursively(const basic_json& source, const basic_json& target, + const string_t& path) { // the patch basic_json result(value_t::array); @@ -5410,7 +5477,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec while (i < source.size() && i < target.size()) { // recursive call to compare array values at index i - auto temp_diff = diff(source[i], target[i], detail::concat(path, '/', detail::to_string(i))); + auto temp_diff = diff_recursively(source[i], target[i], detail::concat(path, '/', detail::to_string(i))); result.insert(result.end(), temp_diff.begin(), temp_diff.end()); ++i; } @@ -5529,7 +5596,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (common_it != common_keys_source_order.cend() && it.key() == *common_it) { const auto path_key = detail::concat(path, '/', detail::escape(it.key())); - auto temp_diff = diff(it.value(), target[it.key()], path_key); + auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key); result.insert(result.end(), temp_diff.begin(), temp_diff.end()); ++common_it; } @@ -5613,6 +5680,331 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return result; } + + /*! + @brief @ref diff without the call stack + + Produces the same patch as @ref diff_recursively. Only used for a source + nested more deeply than @ref diff_depth_limit; any arrays and objects + below it that are not nested that deeply are diffed recursively. + */ + static basic_json diff_iteratively(const basic_json& source, const basic_json& target, + const string_t& path) + { + // the patch + basic_json result(value_t::array); + + // The arrays and objects being diffed are kept on an explicit stack, + // and every pair of elements is still diffed completely before the + // next one, so the operations come out in the same order as in + // diff_recursively. The path of the values being diffed is kept in + // one buffer that grows and shrinks with the stack, rather than in a + // new string per level. + struct diff_frame + { + diff_frame(const basic_json* source_, const basic_json* target_, const std::size_t path_length_) + : source(source_), target(target_), path_length(path_length_) + {} + + /// the values being diffed, both arrays or both objects + const basic_json* source; + const basic_json* target; + /// the length of their path in `current_path` + std::size_t path_length; + /// arrays: the next index to diff + std::size_t index = 0; + /// objects: the next member of source to look at + const_iterator member{}; + /// objects: the keys common to both, in source's order + std::vector common_keys{}; + /// objects: the next entry of common_keys + std::size_t next_common = 0; + /// objects: the "add" operations for keys only target has + basic_json added_ops{}; + }; + std::vector stack; + string_t current_path = path; + + // diff `s` against `t`, whose path is current_path: primitives, + // values of different types, and objects whose members were reordered + // are handled right away; arrays and other objects get a frame + const auto enter = [&result, &stack, ¤t_path](const basic_json & s, const basic_json & t) + { + // if the values are the same, there is nothing to do. Arrays and + // objects are not compared up front: comparing them recurses into + // everything below them - equal ones yield no operations anyway. + if ((!s.is_structured() || !t.is_structured()) && s == t) + { + return; + } + + if (s.type() != t.type()) + { + // different types: replace value + result.push_back( + { + {"op", "replace"}, {"path", current_path}, {"value", t} + }); + return; + } + + // arrays and objects that are not nested too deeply for the call + // stack are diffed recursively, which can skip equal parts + if (!nesting_exceeds(s, diff_depth_limit())) + { + const basic_json partial = diff_recursively(s, t, current_path); + result.insert(result.end(), partial.begin(), partial.end()); + return; + } + + switch (s.type()) + { + case value_t::array: + { + stack.emplace_back(&s, &t, current_path.size()); + return; + } + + case value_t::object: + { + // first pass: record, for every source key, whether it is + // common to both objects (in source's iteration order) or + // was deleted (i.e., in source but not in target) -- this is + // a by-product of the t.find() call already needed to + // tell the two cases apart, so it adds no extra lookups. The + // "remove" ops themselves are emitted later, interleaved + // with the per-key diffs in the fast path below, to match + // source's original iteration order (as the original, + // pre-reordering-aware implementation did) instead of + // grouping all removes before all per-key diffs. + std::vector common_keys_source_order; + for (auto it = s.cbegin(); it != s.cend(); ++it) + { + if (t.find(it.key()) != t.end()) + { + common_keys_source_order.push_back(it.key()); + } + } + + // second pass: find keys that were added (i.e., in target but + // not in source), and record the keys common to both, in + // target's iteration order -- again a by-product of the + // s.find() call already needed to detect added keys. At + // the same time, determine whether every added key comes + // after every common key in target's order (a precondition + // for the fast path below, which only ever appends new keys + // at the very end): for an object_t whose iteration order is + // a pure function of the key set (e.g. the default std::map, + // which always iterates in sorted key order), the order + // check further below is always true and this whole + // mechanism is effectively a no-op; it only matters for a + // reorderable object_t such as the one backing `ordered_json`. + // patch ops for keys that were added (i.e., in target but not + // in source); built here so the fast path below can reuse + // them without a second s.find() per target key. Only + // used by the fast path -- the slow (reordering) path + // rebuilds "add" ops for every key itself. + std::vector common_keys_target_order; + basic_json added_ops(value_t::array); + bool new_keys_form_suffix = true; + bool seen_new_key = false; + for (auto it = t.cbegin(); it != t.cend(); ++it) + { + if (s.find(it.key()) == s.end()) + { + seen_new_key = true; + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + added_ops.push_back( + { + {"op", "add"}, {"path", path_key}, + {"value", it.value()} + }); + } + else + { + common_keys_target_order.push_back(it.key()); + if (seen_new_key) + { + new_keys_form_suffix = false; + } + } + } + + if (common_keys_source_order == common_keys_target_order && new_keys_form_suffix) + { + // fast path: order of common keys already matches (or the + // object_t's iteration order does not depend on + // insertion history), so a plain per-key diff is correct + // and minimal, as before. The frame walks source in + // lockstep with common_keys_source_order, which is, by + // construction, the subsequence of source's keys that + // are common to both objects, in source's iteration + // order -- so a cheap key comparison replaces another + // lookup. Deleted keys are interleaved there too, in + // source's original order, and the "add" ops collected + // above are appended once all members are done. + stack.emplace_back(&s, &t, current_path.size()); + stack.back().member = s.cbegin(); + stack.back().common_keys = std::move(common_keys_source_order); + stack.back().added_ops = std::move(added_ops); + return; + } + + // slow path: the common keys are in a different relative + // order in source and target (only possible for a + // reorderable object_t like ordered_map). Building a + // minimal reordering patch is a nontrivial (LCS-like) + // problem; instead, remove every source key -- both + // deleted keys (which must be removed regardless) and + // common keys (removed so they can be re-added in + // target's order) -- and re-add every key that should + // remain, with its final target value, in target's + // order. basic_json::patch()'s "add" operation on an + // object uses operator[], which appends at the end for a + // vector-backed insertion-ordered map when the key does + // not already exist -- so removing a key and then adding + // it moves it to the end, fixing its position. + for (auto it = s.cbegin(); it != s.cend(); ++it) + { + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + result.push_back(object( + { + {"op", "remove"}, {"path", path_key} + })); + } + + // add every key that is either common (just removed + // above) or brand new, in target's iteration order, so + // that the final order after applying the patch matches + // target exactly + for (auto it = t.cbegin(); it != t.cend(); ++it) + { + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + result.push_back( + { + {"op", "add"}, {"path", path_key}, + {"value", it.value()} + }); + } + return; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + // both primitive types: replace value + result.push_back( + { + {"op", "replace"}, {"path", current_path}, {"value", t} + }); + return; + } + } + }; + + enter(source, target); + while (!stack.empty()) + { + diff_frame& frame = stack.back(); + const std::size_t path_length = frame.path_length; + const std::size_t depth = stack.size(); + + if (frame.source->is_array()) + { + const auto& source_array = *frame.source->m_data.m_value.array; + const auto& target_array = *frame.target->m_data.m_value.array; + + // first pass: traverse common elements + if (frame.index < source_array.size() && frame.index < target_array.size()) + { + const std::size_t i = frame.index++; + detail::concat_into(current_path, '/', detail::to_string(i)); + enter(source_array[i], target_array[i]); // may push, which invalidates `frame` + if (stack.size() == depth) + { + current_path.resize(path_length); + } + continue; + } + + // We now reached the end of at least one array + // in a second pass, traverse the remaining elements + + // remove my remaining elements, highest index first; appending + // in that order avoids the quadratic reinsertion done before + for (std::size_t j = source_array.size(); j > frame.index; --j) + { + result.push_back(object( + { + {"op", "remove"}, + {"path", detail::concat(current_path, '/', detail::to_string(j - 1))} + })); + } + + // add other remaining elements + for (std::size_t i = source_array.size(); i < target_array.size(); ++i) + { + result.push_back( + { + {"op", "add"}, + {"path", detail::concat(current_path, "/-")}, + {"value", target_array[i]} + }); + } + } + else + { + if (frame.member != frame.source->cend()) + { + const const_iterator it = frame.member; + ++frame.member; + if (frame.next_common < frame.common_keys.size() && it.key() == frame.common_keys[frame.next_common]) + { + ++frame.next_common; + const basic_json& target_value = (*frame.target)[it.key()]; + detail::concat_into(current_path, '/', detail::escape(it.key())); + enter(it.value(), target_value); // may push, which invalidates `frame` + if (stack.size() == depth) + { + current_path.resize(path_length); + } + } + else + { + // found a key that is not in target -> remove it + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + result.push_back(object( + { + {"op", "remove"}, {"path", path_key} + })); + } + continue; + } + + // append the "add" ops for brand-new keys collected when the + // object was entered + result.insert(result.end(), frame.added_ops.begin(), frame.added_ops.end()); + } + + // this array or object is done: continue with the one it is in + stack.pop_back(); + if (!stack.empty()) + { + current_path.resize(stack.back().path_length); + } + } + + return result; + } + + public: /// @} //////////////////////////////// diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index c96461458..a10570be6 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -29740,6 +29740,73 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec JSON_HEDLEY_WARN_UNUSED_RESULT static basic_json diff(const basic_json& source, const basic_json& target, const string_t& path = "") + { + // Diffing descends into both values once per nesting level and + // compares them with operator== on the way, which recurses as well, + // so values nested deeply enough used to exhaust the call stack. + // Both only descend as far as the source is nested, so a source + // nested no more than diff_depth_limit() levels deep - all but a + // vanishing minority - is diffed recursively as before; deeper ones + // are diffed without the call stack. + if (JSON_HEDLEY_LIKELY(!nesting_exceeds(source, diff_depth_limit()))) + { + return diff_recursively(source, target, path); + } + return diff_iteratively(source, target, path); + } + + JSON_PRIVATE_UNLESS_TESTED: + /// the nesting depth up to which @ref diff recurses + static constexpr std::size_t diff_depth_limit() noexcept + { + return 128; + } + + private: + /*! + @brief whether @a j is nested more than @a limit levels deep + + A primitive value is not nested at all, an array or object one level more + than its most deeply nested element. Recurses at most @a limit levels. + */ + static bool nesting_exceeds(const basic_json& j, const std::size_t limit) + { + switch (j.m_data.m_type) + { + case value_t::array: + { + return limit == 0 || std::any_of(j.m_data.m_value.array->cbegin(), j.m_data.m_value.array->cend(), + [limit](const basic_json & element) + { + return element.is_structured() && nesting_exceeds(element, limit - 1); + }); + } + + case value_t::object: + { + return limit == 0 || std::any_of(j.m_data.m_value.object->cbegin(), j.m_data.m_value.object->cend(), + [limit](const typename object_t::value_type & element) + { + return element.second.is_structured() && nesting_exceeds(element.second, limit - 1); + }); + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + return false; + } + } + + /// @ref diff for a @a source nested no more than @ref diff_depth_limit levels deep + static basic_json diff_recursively(const basic_json& source, const basic_json& target, + const string_t& path) { // the patch basic_json result(value_t::array); @@ -29769,7 +29836,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec while (i < source.size() && i < target.size()) { // recursive call to compare array values at index i - auto temp_diff = diff(source[i], target[i], detail::concat(path, '/', detail::to_string(i))); + auto temp_diff = diff_recursively(source[i], target[i], detail::concat(path, '/', detail::to_string(i))); result.insert(result.end(), temp_diff.begin(), temp_diff.end()); ++i; } @@ -29888,7 +29955,7 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec if (common_it != common_keys_source_order.cend() && it.key() == *common_it) { const auto path_key = detail::concat(path, '/', detail::escape(it.key())); - auto temp_diff = diff(it.value(), target[it.key()], path_key); + auto temp_diff = diff_recursively(it.value(), target[it.key()], path_key); result.insert(result.end(), temp_diff.begin(), temp_diff.end()); ++common_it; } @@ -29972,6 +30039,331 @@ class basic_json // NOLINT(cppcoreguidelines-special-member-functions,hicpp-spec return result; } + + /*! + @brief @ref diff without the call stack + + Produces the same patch as @ref diff_recursively. Only used for a source + nested more deeply than @ref diff_depth_limit; any arrays and objects + below it that are not nested that deeply are diffed recursively. + */ + static basic_json diff_iteratively(const basic_json& source, const basic_json& target, + const string_t& path) + { + // the patch + basic_json result(value_t::array); + + // The arrays and objects being diffed are kept on an explicit stack, + // and every pair of elements is still diffed completely before the + // next one, so the operations come out in the same order as in + // diff_recursively. The path of the values being diffed is kept in + // one buffer that grows and shrinks with the stack, rather than in a + // new string per level. + struct diff_frame + { + diff_frame(const basic_json* source_, const basic_json* target_, const std::size_t path_length_) + : source(source_), target(target_), path_length(path_length_) + {} + + /// the values being diffed, both arrays or both objects + const basic_json* source; + const basic_json* target; + /// the length of their path in `current_path` + std::size_t path_length; + /// arrays: the next index to diff + std::size_t index = 0; + /// objects: the next member of source to look at + const_iterator member{}; + /// objects: the keys common to both, in source's order + std::vector common_keys{}; + /// objects: the next entry of common_keys + std::size_t next_common = 0; + /// objects: the "add" operations for keys only target has + basic_json added_ops{}; + }; + std::vector stack; + string_t current_path = path; + + // diff `s` against `t`, whose path is current_path: primitives, + // values of different types, and objects whose members were reordered + // are handled right away; arrays and other objects get a frame + const auto enter = [&result, &stack, ¤t_path](const basic_json & s, const basic_json & t) + { + // if the values are the same, there is nothing to do. Arrays and + // objects are not compared up front: comparing them recurses into + // everything below them - equal ones yield no operations anyway. + if ((!s.is_structured() || !t.is_structured()) && s == t) + { + return; + } + + if (s.type() != t.type()) + { + // different types: replace value + result.push_back( + { + {"op", "replace"}, {"path", current_path}, {"value", t} + }); + return; + } + + // arrays and objects that are not nested too deeply for the call + // stack are diffed recursively, which can skip equal parts + if (!nesting_exceeds(s, diff_depth_limit())) + { + const basic_json partial = diff_recursively(s, t, current_path); + result.insert(result.end(), partial.begin(), partial.end()); + return; + } + + switch (s.type()) + { + case value_t::array: + { + stack.emplace_back(&s, &t, current_path.size()); + return; + } + + case value_t::object: + { + // first pass: record, for every source key, whether it is + // common to both objects (in source's iteration order) or + // was deleted (i.e., in source but not in target) -- this is + // a by-product of the t.find() call already needed to + // tell the two cases apart, so it adds no extra lookups. The + // "remove" ops themselves are emitted later, interleaved + // with the per-key diffs in the fast path below, to match + // source's original iteration order (as the original, + // pre-reordering-aware implementation did) instead of + // grouping all removes before all per-key diffs. + std::vector common_keys_source_order; + for (auto it = s.cbegin(); it != s.cend(); ++it) + { + if (t.find(it.key()) != t.end()) + { + common_keys_source_order.push_back(it.key()); + } + } + + // second pass: find keys that were added (i.e., in target but + // not in source), and record the keys common to both, in + // target's iteration order -- again a by-product of the + // s.find() call already needed to detect added keys. At + // the same time, determine whether every added key comes + // after every common key in target's order (a precondition + // for the fast path below, which only ever appends new keys + // at the very end): for an object_t whose iteration order is + // a pure function of the key set (e.g. the default std::map, + // which always iterates in sorted key order), the order + // check further below is always true and this whole + // mechanism is effectively a no-op; it only matters for a + // reorderable object_t such as the one backing `ordered_json`. + // patch ops for keys that were added (i.e., in target but not + // in source); built here so the fast path below can reuse + // them without a second s.find() per target key. Only + // used by the fast path -- the slow (reordering) path + // rebuilds "add" ops for every key itself. + std::vector common_keys_target_order; + basic_json added_ops(value_t::array); + bool new_keys_form_suffix = true; + bool seen_new_key = false; + for (auto it = t.cbegin(); it != t.cend(); ++it) + { + if (s.find(it.key()) == s.end()) + { + seen_new_key = true; + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + added_ops.push_back( + { + {"op", "add"}, {"path", path_key}, + {"value", it.value()} + }); + } + else + { + common_keys_target_order.push_back(it.key()); + if (seen_new_key) + { + new_keys_form_suffix = false; + } + } + } + + if (common_keys_source_order == common_keys_target_order && new_keys_form_suffix) + { + // fast path: order of common keys already matches (or the + // object_t's iteration order does not depend on + // insertion history), so a plain per-key diff is correct + // and minimal, as before. The frame walks source in + // lockstep with common_keys_source_order, which is, by + // construction, the subsequence of source's keys that + // are common to both objects, in source's iteration + // order -- so a cheap key comparison replaces another + // lookup. Deleted keys are interleaved there too, in + // source's original order, and the "add" ops collected + // above are appended once all members are done. + stack.emplace_back(&s, &t, current_path.size()); + stack.back().member = s.cbegin(); + stack.back().common_keys = std::move(common_keys_source_order); + stack.back().added_ops = std::move(added_ops); + return; + } + + // slow path: the common keys are in a different relative + // order in source and target (only possible for a + // reorderable object_t like ordered_map). Building a + // minimal reordering patch is a nontrivial (LCS-like) + // problem; instead, remove every source key -- both + // deleted keys (which must be removed regardless) and + // common keys (removed so they can be re-added in + // target's order) -- and re-add every key that should + // remain, with its final target value, in target's + // order. basic_json::patch()'s "add" operation on an + // object uses operator[], which appends at the end for a + // vector-backed insertion-ordered map when the key does + // not already exist -- so removing a key and then adding + // it moves it to the end, fixing its position. + for (auto it = s.cbegin(); it != s.cend(); ++it) + { + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + result.push_back(object( + { + {"op", "remove"}, {"path", path_key} + })); + } + + // add every key that is either common (just removed + // above) or brand new, in target's iteration order, so + // that the final order after applying the patch matches + // target exactly + for (auto it = t.cbegin(); it != t.cend(); ++it) + { + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + result.push_back( + { + {"op", "add"}, {"path", path_key}, + {"value", it.value()} + }); + } + return; + } + + case value_t::null: + case value_t::string: + case value_t::boolean: + case value_t::number_integer: + case value_t::number_unsigned: + case value_t::number_float: + case value_t::binary: + case value_t::discarded: + default: + { + // both primitive types: replace value + result.push_back( + { + {"op", "replace"}, {"path", current_path}, {"value", t} + }); + return; + } + } + }; + + enter(source, target); + while (!stack.empty()) + { + diff_frame& frame = stack.back(); + const std::size_t path_length = frame.path_length; + const std::size_t depth = stack.size(); + + if (frame.source->is_array()) + { + const auto& source_array = *frame.source->m_data.m_value.array; + const auto& target_array = *frame.target->m_data.m_value.array; + + // first pass: traverse common elements + if (frame.index < source_array.size() && frame.index < target_array.size()) + { + const std::size_t i = frame.index++; + detail::concat_into(current_path, '/', detail::to_string(i)); + enter(source_array[i], target_array[i]); // may push, which invalidates `frame` + if (stack.size() == depth) + { + current_path.resize(path_length); + } + continue; + } + + // We now reached the end of at least one array + // in a second pass, traverse the remaining elements + + // remove my remaining elements, highest index first; appending + // in that order avoids the quadratic reinsertion done before + for (std::size_t j = source_array.size(); j > frame.index; --j) + { + result.push_back(object( + { + {"op", "remove"}, + {"path", detail::concat(current_path, '/', detail::to_string(j - 1))} + })); + } + + // add other remaining elements + for (std::size_t i = source_array.size(); i < target_array.size(); ++i) + { + result.push_back( + { + {"op", "add"}, + {"path", detail::concat(current_path, "/-")}, + {"value", target_array[i]} + }); + } + } + else + { + if (frame.member != frame.source->cend()) + { + const const_iterator it = frame.member; + ++frame.member; + if (frame.next_common < frame.common_keys.size() && it.key() == frame.common_keys[frame.next_common]) + { + ++frame.next_common; + const basic_json& target_value = (*frame.target)[it.key()]; + detail::concat_into(current_path, '/', detail::escape(it.key())); + enter(it.value(), target_value); // may push, which invalidates `frame` + if (stack.size() == depth) + { + current_path.resize(path_length); + } + } + else + { + // found a key that is not in target -> remove it + const auto path_key = detail::concat(current_path, '/', detail::escape(it.key())); + result.push_back(object( + { + {"op", "remove"}, {"path", path_key} + })); + } + continue; + } + + // append the "add" ops for brand-new keys collected when the + // object was entered + result.insert(result.end(), frame.added_ops.begin(), frame.added_ops.end()); + } + + // this array or object is done: continue with the one it is in + stack.pop_back(); + if (!stack.empty()) + { + current_path.resize(stack.back().path_length); + } + } + + return result; + } + + public: /// @} //////////////////////////////// diff --git a/tests/src/unit-json_patch.cpp b/tests/src/unit-json_patch.cpp index 7731c7d92..34ce492df 100644 --- a/tests/src/unit-json_patch.cpp +++ b/tests/src/unit-json_patch.cpp @@ -15,8 +15,64 @@ using nlohmann::json; #endif #include +#include #include "make_test_data_available.hpp" +namespace +{ +// alternating objects and arrays nested `depth` levels deep, with members that +// depend on `variant` at some levels, so diffing two variants yields +// operations on many levels: replacing the innermost value, adding, removing, +// and (for ordered_json) reordering members, and changing array lengths +template +BasicJsonType nested(const std::size_t depth, const int variant) +{ + BasicJsonType value = variant; + for (std::size_t i = 0; i < depth; ++i) + { + if (i % 2 == 0) + { + BasicJsonType object = BasicJsonType::object(); + if ((i + static_cast(variant)) % 7 == 0) + { + object["x"] = i; + } + if (variant == 2 && i % 11 == 0) + { + object["z"] = "z"; + } + object["a"] = std::move(value); + if (variant == 1 && i % 5 == 0) + { + object["y"] = 1; + } + value = std::move(object); + } + else + { + BasicJsonType array = BasicJsonType::array({std::move(value)}); + if ((i + static_cast(variant)) % 3 == 0) + { + array.push_back(i); + } + value = std::move(array); + } + } + return value; +} + +// a path of `depth` reference tokens, as nested() nests its values +std::string nested_path(const std::size_t depth) +{ + std::string path; + for (std::size_t i = depth; i > 0; --i) + { + path += (i - 1) % 2 == 0 ? "/a" : "/0"; + } + return path; +} +} // namespace + TEST_CASE("JSON patch") { SECTION("examples from RFC 6902") @@ -1751,3 +1807,87 @@ TEST_CASE("JSON patch - diff emits array removals in descending index order") CHECK(source.patch(patch) == target); } } + +TEST_CASE("JSON patch: diff of deeply nested values") +{ + SECTION("the diff reproduces the target at every depth") + { + // every depth on either side of the nesting depth up to which diff() + // recurses (basic_json::diff_depth_limit(), 128) + for (std::size_t depth = 0; depth <= 300; ++depth) + { + CAPTURE(depth); + for (int from = 0; from < 3; ++from) + { + for (int to = 0; to < 3; ++to) + { + CAPTURE(from); + CAPTURE(to); + const auto source = nested(depth, from); + const auto target = nested(depth, to); + const auto patch = json::diff(source, target); + CHECK(source.patch(patch) == target); + CHECK(patch.empty() == (from == to)); + + const auto ordered_source = nested(depth, from); + const auto ordered_target = nested(depth, to); + CHECK(ordered_source.patch(nlohmann::ordered_json::diff(ordered_source, ordered_target)) == ordered_target); + } + } + } + } + + SECTION("a difference only in the innermost value is one replace operation") + { + for (std::size_t depth = 0; depth <= 300; ++depth) + { + CAPTURE(depth); + json source = 1; + json target = 2; + for (std::size_t i = 0; i < depth; ++i) + { + source = i % 2 == 0 ? json::object({{"a", std::move(source)}}) : json::array({std::move(source)}); + target = i % 2 == 0 ? json::object({{"a", std::move(target)}}) : json::array({std::move(target)}); + } + CHECK(json::diff(source, target, "/root") == json::array({{{"op", "replace"}, {"path", "/root" + nested_path(depth)}, {"value", 2}}})); + } + } + + SECTION("values nested too deeply for the call stack (#5393)") + { + // diff() used to recurse once per nesting level, and compared the + // values with operator== on every level. The values are only + // parsed and diffed, never copied or compared, since those recurse + // too. + const std::size_t depth = 100000; + for (const bool objects : + { + false, true + }) + { + CAPTURE(objects); + std::string source_text; + std::string target_text; + std::string equal_text; + std::string path; + for (std::size_t i = 0; i < depth; ++i) + { + source_text += objects ? "{\"a\":" : "["; + path += objects ? "/a" : "/0"; + } + target_text = source_text + "2"; + equal_text = source_text + "1"; + source_text += "1"; + const std::string closing(depth, objects ? '}' : ']'); + const auto source = json::parse(source_text + closing); + + const auto patch = json::diff(source, json::parse(target_text + closing)); + REQUIRE(patch.size() == 1); + CHECK(patch[0]["op"] == "replace"); + CHECK(patch[0]["path"] == path); + CHECK(patch[0]["value"] == 2); + + CHECK(json::diff(source, json::parse(equal_text + closing)).empty()); + } + } +}