diff --git a/docs/mkdocs/docs/api/basic_json/diff.md b/docs/mkdocs/docs/api/basic_json/diff.md index ae22454dd..c4e710a7d 100644 --- a/docs/mkdocs/docs/api/basic_json/diff.md +++ b/docs/mkdocs/docs/api/basic_json/diff.md @@ -59,6 +59,7 @@ Currently, only `remove`, `add`, and `replace` operations are generated. - [patch](patch.md) applies a JSON Patch - [patch_inplace](patch_inplace.md) applies a JSON Patch in place - [merge_patch](merge_patch.md) applies a JSON Merge Patch +- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch ## Version history diff --git a/docs/mkdocs/docs/api/basic_json/index.md b/docs/mkdocs/docs/api/basic_json/index.md index fdb57105c..ee40bc2b0 100644 --- a/docs/mkdocs/docs/api/basic_json/index.md +++ b/docs/mkdocs/docs/api/basic_json/index.md @@ -285,6 +285,7 @@ Access to the JSON value ### JSON Merge Patch functions - [**merge_patch**](merge_patch.md) - applies a JSON Merge Patch +- [**merge_diff**](merge_diff.md) (_static_) - creates a diff as a JSON Merge Patch ## Static functions diff --git a/docs/mkdocs/docs/api/basic_json/merge_diff.md b/docs/mkdocs/docs/api/basic_json/merge_diff.md new file mode 100644 index 000000000..b606dbad0 --- /dev/null +++ b/docs/mkdocs/docs/api/basic_json/merge_diff.md @@ -0,0 +1,79 @@ +# nlohmann::basic_json::merge_diff + +```cpp +static basic_json merge_diff(const basic_json& source, + const basic_json& target); +``` + +Creates a [JSON Merge Patch](https://tools.ietf.org/html/rfc7396) so that value `source` can be changed into the value +`target` by calling the [`merge_patch`](merge_patch.md) function. The patch contains only what differs: changed and +added members with their new values, and removed members with the value `#!json null`. Nested objects are compared +member by member; any other value that differs, including an array, is replaced as a whole. + +For two JSON values `source` and `target`, where `target` contains no object member whose value is `#!json null`, the +following code always yields `#!cpp true`: +```cpp +basic_json patched = source; +patched.merge_patch(merge_diff(source, target)); +patched == target; +``` + +## Parameters + +`source` (in) +: JSON value to compare from + +`target` (in) +: JSON value to compare against + +## Return value + +a JSON Merge Patch to convert the `source` to `target` + +## Exception safety + +Strong guarantee: `source` and `target` are never modified. + +## Complexity + +Linear in the total number of values (including nested ones) in `source` and `target`, times the cost of a key-based +object operation (looking up a key, or adding one to the patch): logarithmic for [`json`](../json.md), and linear for +[`ordered_json`](../ordered_json.md) (see [`ordered_map` complexity](../ordered_map.md#complexity)). For +`ordered_json`, diffing two objects with `n` and `m` members therefore takes O((n+m)²). + +## Notes + +- A merge patch uses `#!json null` to remove a member, so it cannot set a member to `#!json null`. As in RFC 7396, a + `#!json null` member of `target` is treated as absent: the patch removes the member from `source` instead of setting + it to `#!json null`. A member that is `#!json null` in both `source` and `target` is unchanged and left out of the + patch, so `source` keeps it. +- A merge patch cannot reorder object members, and [`merge_patch`](merge_patch.md) appends added members at the end. + For [`ordered_json`](../ordered_json.md), the patched value therefore has the same members as `target`, but not + necessarily in the same order, and may compare unequal to it. The patch lists changed and removed members in the + order of `source`, followed by added members in the order of `target`. + +## Examples + +??? example + + The following code shows how a JSON Merge Patch is created as a diff for two JSON values. + + ```cpp + --8<-- "examples/merge_diff.cpp" + ``` + + Output: + + ```json + --8<-- "examples/merge_diff.output" + ``` + +## See also + +- [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396) +- [merge_patch](merge_patch.md) applies a JSON Merge Patch +- [diff](diff.md) creates a diff as a JSON Patch + +## Version history + +- Added in version 3.13.0. diff --git a/docs/mkdocs/docs/api/basic_json/merge_patch.md b/docs/mkdocs/docs/api/basic_json/merge_patch.md index dbda23f42..00e1bc8e5 100644 --- a/docs/mkdocs/docs/api/basic_json/merge_patch.md +++ b/docs/mkdocs/docs/api/basic_json/merge_patch.md @@ -67,6 +67,7 @@ returned by `#!cpp (*this)[key]`); it is read as it was when `merge_patch()` was - [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396) - [patch](patch.md) apply a JSON patch +- [merge_diff](merge_diff.md) creates a diff as a JSON Merge Patch ## Version history diff --git a/docs/mkdocs/docs/examples/merge_diff.cpp b/docs/mkdocs/docs/examples/merge_diff.cpp new file mode 100644 index 000000000..22ae9b087 --- /dev/null +++ b/docs/mkdocs/docs/examples/merge_diff.cpp @@ -0,0 +1,51 @@ +#include +#include +#include + +using json = nlohmann::json; +using namespace nlohmann::literals; + +int main() +{ + // the source document + json source = R"( + { + "title": "Goodbye!", + "author": { + "givenName": "John", + "familyName": "Doe" + }, + "tags": [ + "example", + "sample" + ], + "content": "This will be unchanged" + } + )"_json; + + // the target document + json target = R"( + { + "title": "Hello!", + "author": { + "givenName": "John" + }, + "tags": [ + "example" + ], + "content": "This will be unchanged", + "phoneNumber": "+01-123-456-7890" + } + )"_json; + + // create the patch + json patch = json::merge_diff(source, target); + + // roundtrip + json patched_source = source; + patched_source.merge_patch(patch); + + // output patch and roundtrip result + std::cout << std::setw(4) << patch << "\n\n" + << std::setw(4) << patched_source << std::endl; +} diff --git a/docs/mkdocs/docs/examples/merge_diff.output b/docs/mkdocs/docs/examples/merge_diff.output new file mode 100644 index 000000000..ca7c59bf2 --- /dev/null +++ b/docs/mkdocs/docs/examples/merge_diff.output @@ -0,0 +1,22 @@ +{ + "author": { + "familyName": null + }, + "phoneNumber": "+01-123-456-7890", + "tags": [ + "example" + ], + "title": "Hello!" +} + +{ + "author": { + "givenName": "John" + }, + "content": "This will be unchanged", + "phoneNumber": "+01-123-456-7890", + "tags": [ + "example" + ], + "title": "Hello!" +} diff --git a/docs/mkdocs/docs/features/merge_patch.md b/docs/mkdocs/docs/features/merge_patch.md index c0a316e65..396ea87dc 100644 --- a/docs/mkdocs/docs/features/merge_patch.md +++ b/docs/mkdocs/docs/features/merge_patch.md @@ -9,6 +9,9 @@ syntax that closely mimics the document being modified. Unlike [JSON Patch](json express every kind of change (e.g., it cannot reorder array elements or remove a specific array element), but it is easier to read and write for object-shaped documents. +The function [`merge_diff`](../api/basic_json/merge_diff.md) creates a merge patch that changes one JSON value into +another. + !!! tip "Not a general deep merge" A merge patch is not a general deep merge: a `#!json null` value in the patch deletes the key from the target. @@ -35,4 +38,5 @@ easier to read and write for object-shaped documents. - [JSON Patch and Diff](json_patch.md) - a more expressive alternative that describes a sequence of operations - [JSON Pointer](json_pointer.md) - the addressing scheme used by JSON Patch - Function [`merge_patch`](../api/basic_json/merge_patch.md) +- Function [`merge_diff`](../api/basic_json/merge_diff.md) - Function [`update`](../api/basic_json/update.md) - merge objects, optionally recursively diff --git a/docs/mkdocs/mkdocs.yml b/docs/mkdocs/mkdocs.yml index 111e0da81..74b6c4d46 100644 --- a/docs/mkdocs/mkdocs.yml +++ b/docs/mkdocs/mkdocs.yml @@ -182,6 +182,7 @@ nav: - 'json_serializer': api/basic_json/json_serializer.md - 'max_size': api/basic_json/max_size.md - 'meta': api/basic_json/meta.md + - 'merge_diff': api/basic_json/merge_diff.md - 'merge_patch': api/basic_json/merge_patch.md - 'number_float_t': api/basic_json/number_float_t.md - 'number_integer_t': api/basic_json/number_integer_t.md diff --git a/include/nlohmann/json.hpp b/include/nlohmann/json.hpp index b91fa3e27..fae89829a 100644 --- a/include/nlohmann/json.hpp +++ b/include/nlohmann/json.hpp @@ -7449,6 +7449,176 @@ public: } public: + /// @brief creates a diff as a JSON Merge Patch + /// @sa https://json.nlohmann.me/api/basic_json/merge_diff/ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json merge_diff(const basic_json& source, const basic_json& target) + { + // unless both are objects, the patch is the target itself + if (!source.is_object() || !target.is_object()) + { + return target; + } + + return merge_diff_recursively(source, target, 0); + } + + private: + /// @brief two objects @ref merge_diff_iteratively is diffing + struct merge_diff_frame + { + merge_diff_frame(const basic_json* source_, const basic_json* target_) + : source(source_), target(target_), member(source_->cbegin()), patch(value_t::object) + {} + + // declared for GCC's -Weffc++, as in diff_frame + merge_diff_frame(const merge_diff_frame&) = default; + merge_diff_frame(merge_diff_frame&&) = default; + merge_diff_frame& operator=(const merge_diff_frame&) = default; + merge_diff_frame& operator=(merge_diff_frame&&) = default; + ~merge_diff_frame() = default; + + /// the objects being diffed + const basic_json* source; + const basic_json* target; + /// the member of source being diffed; advanced once it is done + const_iterator member; + /// the patch for the members of source diffed so far + basic_json patch; + }; + + /// @brief add the members of object @a target that @a source does not + /// have to the merge patch @a result + static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target) + { + for (auto it = target.begin(); it != target.end(); ++it) + { + auto itf = source.find(it.key()); + if (itf == source.end()) + { + result[it.key()] = it.value(); + } + } + } + + /*! + @brief @ref merge_diff of two objects at nesting level @a depth + + Diffing two nested objects calls this function again, once per nesting + level, so values nested deeply enough would exhaust the call stack and + terminate the process. The descent is bounded here: once @ref + detail::recursion_depth_limit levels have been entered, @ref + merge_diff_iteratively diffs what is left without the call stack. + */ + static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth) + { + JSON_ASSERT(source.is_object() && target.is_object()); + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return merge_diff_iteratively(source, target); + } + + basic_json result(value_t::object); + + for (auto it = source.begin(); it != source.end(); ++it) + { + auto itf = target.find(it.key()); + if (itf != target.end()) + { + if (it.value().is_object() && itf.value().is_object()) + { + auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1); + if (!diff.empty()) + { + result[it.key()] = std::move(diff); + } + } + else if (it.value() != itf.value()) + { + result[it.key()] = itf.value(); + } + } + else + { + result[it.key()] = value_t::null; + } + } + + merge_diff_added_members(result, source, target); + return result; + } + + /*! + @brief @ref merge_diff of two objects without the call stack + + Produces the same patch as @ref merge_diff_recursively, keeping the objects + being diffed on an explicit stack instead of descending into them. Two + nested objects are still diffed completely before the next member, so the + patch has its members in the same order as in the recursive version. Only + reached for objects nested more deeply than @ref + detail::recursion_depth_limit. + */ + static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target) + { + // the patch of source and target, set once their frame is popped + basic_json result; + std::vector stack; + stack.emplace_back(&source, &target); + while (!stack.empty()) + { + // the frame's members are copied out and it is changed through + // stack.back(): pushing a frame for nested objects or popping + // this one would invalidate a reference to it + const basic_json* const s = stack.back().source; + const basic_json* const t = stack.back().target; + const const_iterator it = stack.back().member; + + if (it != s->cend()) + { + const const_iterator itf = t->find(it.key()); + if (itf != t->cend()) + { + if (it.value().is_object() && itf.value().is_object()) + { + // member is advanced once their patch is under its key + stack.emplace_back(&it.value(), &itf.value()); + continue; + } + if (it.value() != itf.value()) + { + stack.back().patch[it.key()] = itf.value(); + } + } + else + { + stack.back().patch[it.key()] = value_t::null; + } + ++stack.back().member; + continue; + } + + merge_diff_added_members(stack.back().patch, *s, *t); + + // these objects are done: continue with the ones they are in + basic_json patch = std::move(stack.back().patch); + stack.pop_back(); + if (stack.empty()) + { + result = std::move(patch); + } + else + { + if (!patch.empty()) + { + stack.back().patch[stack.back().member.key()] = std::move(patch); + } + ++stack.back().member; + } + } + return result; + } + /// @} }; diff --git a/single_include/nlohmann/json.hpp b/single_include/nlohmann/json.hpp index d8f7e41b3..b6b927835 100644 --- a/single_include/nlohmann/json.hpp +++ b/single_include/nlohmann/json.hpp @@ -35570,6 +35570,176 @@ public: } public: + /// @brief creates a diff as a JSON Merge Patch + /// @sa https://json.nlohmann.me/api/basic_json/merge_diff/ + JSON_HEDLEY_WARN_UNUSED_RESULT + static basic_json merge_diff(const basic_json& source, const basic_json& target) + { + // unless both are objects, the patch is the target itself + if (!source.is_object() || !target.is_object()) + { + return target; + } + + return merge_diff_recursively(source, target, 0); + } + + private: + /// @brief two objects @ref merge_diff_iteratively is diffing + struct merge_diff_frame + { + merge_diff_frame(const basic_json* source_, const basic_json* target_) + : source(source_), target(target_), member(source_->cbegin()), patch(value_t::object) + {} + + // declared for GCC's -Weffc++, as in diff_frame + merge_diff_frame(const merge_diff_frame&) = default; + merge_diff_frame(merge_diff_frame&&) = default; + merge_diff_frame& operator=(const merge_diff_frame&) = default; + merge_diff_frame& operator=(merge_diff_frame&&) = default; + ~merge_diff_frame() = default; + + /// the objects being diffed + const basic_json* source; + const basic_json* target; + /// the member of source being diffed; advanced once it is done + const_iterator member; + /// the patch for the members of source diffed so far + basic_json patch; + }; + + /// @brief add the members of object @a target that @a source does not + /// have to the merge patch @a result + static void merge_diff_added_members(basic_json& result, const basic_json& source, const basic_json& target) + { + for (auto it = target.begin(); it != target.end(); ++it) + { + auto itf = source.find(it.key()); + if (itf == source.end()) + { + result[it.key()] = it.value(); + } + } + } + + /*! + @brief @ref merge_diff of two objects at nesting level @a depth + + Diffing two nested objects calls this function again, once per nesting + level, so values nested deeply enough would exhaust the call stack and + terminate the process. The descent is bounded here: once @ref + detail::recursion_depth_limit levels have been entered, @ref + merge_diff_iteratively diffs what is left without the call stack. + */ + static basic_json merge_diff_recursively(const basic_json& source, const basic_json& target, const std::size_t depth) + { + JSON_ASSERT(source.is_object() && target.is_object()); + + if (JSON_HEDLEY_UNLIKELY(depth >= detail::recursion_depth_limit())) + { + return merge_diff_iteratively(source, target); + } + + basic_json result(value_t::object); + + for (auto it = source.begin(); it != source.end(); ++it) + { + auto itf = target.find(it.key()); + if (itf != target.end()) + { + if (it.value().is_object() && itf.value().is_object()) + { + auto diff = merge_diff_recursively(it.value(), itf.value(), depth + 1); + if (!diff.empty()) + { + result[it.key()] = std::move(diff); + } + } + else if (it.value() != itf.value()) + { + result[it.key()] = itf.value(); + } + } + else + { + result[it.key()] = value_t::null; + } + } + + merge_diff_added_members(result, source, target); + return result; + } + + /*! + @brief @ref merge_diff of two objects without the call stack + + Produces the same patch as @ref merge_diff_recursively, keeping the objects + being diffed on an explicit stack instead of descending into them. Two + nested objects are still diffed completely before the next member, so the + patch has its members in the same order as in the recursive version. Only + reached for objects nested more deeply than @ref + detail::recursion_depth_limit. + */ + static basic_json merge_diff_iteratively(const basic_json& source, const basic_json& target) + { + // the patch of source and target, set once their frame is popped + basic_json result; + std::vector stack; + stack.emplace_back(&source, &target); + while (!stack.empty()) + { + // the frame's members are copied out and it is changed through + // stack.back(): pushing a frame for nested objects or popping + // this one would invalidate a reference to it + const basic_json* const s = stack.back().source; + const basic_json* const t = stack.back().target; + const const_iterator it = stack.back().member; + + if (it != s->cend()) + { + const const_iterator itf = t->find(it.key()); + if (itf != t->cend()) + { + if (it.value().is_object() && itf.value().is_object()) + { + // member is advanced once their patch is under its key + stack.emplace_back(&it.value(), &itf.value()); + continue; + } + if (it.value() != itf.value()) + { + stack.back().patch[it.key()] = itf.value(); + } + } + else + { + stack.back().patch[it.key()] = value_t::null; + } + ++stack.back().member; + continue; + } + + merge_diff_added_members(stack.back().patch, *s, *t); + + // these objects are done: continue with the ones they are in + basic_json patch = std::move(stack.back().patch); + stack.pop_back(); + if (stack.empty()) + { + result = std::move(patch); + } + else + { + if (!patch.empty()) + { + stack.back().patch[stack.back().member.key()] = std::move(patch); + } + ++stack.back().member; + } + } + return result; + } + /// @} }; diff --git a/tests/src/unit-diagnostics.cpp b/tests/src/unit-diagnostics.cpp index 1ada023b7..37427587c 100644 --- a/tests/src/unit-diagnostics.cpp +++ b/tests/src/unit-diagnostics.cpp @@ -567,9 +567,9 @@ TEST_CASE("Regression tests for extended diagnostics") } } -TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch()") +TEST_CASE("Better diagnostics past the descent bound of update(), merge_patch() and merge_diff()") { - // Both merge objects nested more than detail::recursion_depth_limit() + // All three handle objects nested more than detail::recursion_depth_limit() // (128) levels deep without recursing; the values they add or replace // there must still know their parents. // The values are built rather than parsed, so that the expected messages @@ -615,4 +615,19 @@ TEST_CASE("Better diagnostics past the descent bound of update() and merge_patch CHECK_THROWS_WITH_AS(p->at("x").at(0), expected_x.c_str(), json::type_error); CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error); } + + SECTION("merge_diff()") + { + // target and patch differ only in their innermost members, so the + // diff of the two is nested as deeply: {"a": ... {"x": null, "y": 2}} + const json diff = json::merge_diff(target, patch); // NOLINT(readability-suspicious-call-argument) + + const json* p = &diff; + for (std::size_t i = 0; i < depth; ++i) + { + p = &p->at("a"); + } + CHECK(p->at("x").is_null()); + CHECK_THROWS_WITH_AS(p->at("y").at(0), expected_y.c_str(), json::type_error); + } } diff --git a/tests/src/unit-merge_diff.cpp b/tests/src/unit-merge_diff.cpp new file mode 100644 index 000000000..77fa58768 --- /dev/null +++ b/tests/src/unit-merge_diff.cpp @@ -0,0 +1,1024 @@ +// __ _____ _____ _____ +// __| | __| | | | 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 +using nlohmann::json; +using nlohmann::ordered_json; +#ifdef JSON_TEST_NO_GLOBAL_UDLS + using namespace nlohmann::literals; // NOLINT(google-build-using-namespace) +#endif + +#include +#include + +namespace +{ +// merge_diff() recursing once per nesting level, without the bound on its +// descent; only usable on values nested a few hundred levels deep +template +BasicJsonType reference_merge_diff(const BasicJsonType& source, const BasicJsonType& target) +{ + if (!source.is_object() || !target.is_object()) + { + return target; + } + + BasicJsonType result = BasicJsonType::object(); + for (auto it = source.begin(); it != source.end(); ++it) + { + const auto itf = target.find(it.key()); + if (itf == target.end()) + { + result[it.key()] = nullptr; + } + else if (it.value().is_object() && itf.value().is_object()) + { + auto diff = reference_merge_diff(it.value(), itf.value()); + if (!diff.empty()) + { + result[it.key()] = std::move(diff); + } + } + else if (it.value() != itf.value()) + { + result[it.key()] = itf.value(); + } + } + for (auto it = target.begin(); it != target.end(); ++it) + { + if (source.find(it.key()) == source.end()) + { + result[it.key()] = it.value(); + } + } + return result; +} + +// objects nested `depth` levels deep under the key "a", with members before +// and after it that differ by `variant` on the way down, so that diffing two +// variants removes, adds and replaces members on many levels, and diffs nested +// objects that are equal, that differ, or that are not objects in one of the +// variants; only variant 2 has members that are null +template +BasicJsonType nested_objects(const std::size_t depth, const int variant) +{ + BasicJsonType value = BasicJsonType::object({{"v", variant}}); + for (std::size_t i = 0; i < depth; ++i) + { + BasicJsonType object = BasicJsonType::object(); + if ((i + static_cast(variant)) % 3 == 0) + { + object["s" + std::to_string(variant)] = i; + } + object["c"] = i % 4 == 0 ? variant : 0; + if (i % 5 == 0) + { + if (variant == 0) + { + object["o"] = BasicJsonType::object({{"p", i}}); + } + else if (variant == 1) + { + object["o"] = BasicJsonType::array({i}); + } + else + { + object["o"] = nullptr; + } + } + if (variant == 2 && i % 4 == 2) + { + object["n"] = nullptr; + } + object["a"] = std::move(value); + if (i % 6 == 0) + { + object["d"] = BasicJsonType::object({{"e", variant == 1 ? 1 : 0}}); + } + if (variant == 1 && i % 7 == 0) + { + object["z"] = "z"; + } + value = std::move(object); + } + return value; +} +} // namespace + +TEST_CASE("JSON Merge Patch diff") +{ + SECTION("examples from RFC 7396") + { + SECTION("Section 1") + { + json document = R"({ + "a": "b", + "c": { + "d": "e", + "f": "g" + } + })"_json; + + json expected = R"({ + "a": "z", + "c": { + "d": "e" + } + })"_json; + + json const expected_patch = R"({ + "a": "z", + "c": { + "f": null + } + })"_json; + + auto patch = json::merge_diff(document, expected); + CHECK(patch == expected_patch); + + document.merge_patch(patch); + CHECK(document == expected); + } + + SECTION("Section 3") + { + json document = R"({ + "title": "Goodbye!", + "author": { + "givenName": "John", + "familyName": "Doe" + }, + "tags": [ + "example", + "sample" + ], + "content": "This will be unchanged" + })"_json; + + json expected = R"({ + "title": "Hello!", + "author": { + "givenName": "John" + }, + "tags": [ + "example" + ], + "content": "This will be unchanged", + "phoneNumber": "+01-123-456-7890" + })"_json; + + json const expected_patch = R"({ + "title": "Hello!", + "phoneNumber": "+01-123-456-7890", + "author": { + "familyName": null + }, + "tags": [ + "example" + ] + })"_json; + + auto patch = json::merge_diff(document, expected); + CHECK(patch == expected_patch); + + document.merge_patch(patch); + CHECK(document == expected); + } + + SECTION("Appendix A") + { + SECTION("Example 1") + { + json original = R"({"a":"b"})"_json; + json result = R"({"a":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":"c"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 2") + { + json original = R"({"a":"b"})"_json; + json result = R"({"a":"b", "b":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"b":"c"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 3") + { + json original = R"({"a":"b"})"_json; + json result = R"({})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 4") + { + json original = R"({"a":"b","b":"c"})"_json; + json result = R"({"b":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 5") + { + json original = R"({"a":["b"]})"_json; + json result = R"({"a":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":"c"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 6") + { + json original = R"({"a":"c"})"_json; + json result = R"({"a":["b"]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":["b"]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 7") + { + json original = R"({"a":{"b": "c"}})"_json; + json result = R"({"a": {"b": "d"}})"_json; + + auto patch = json::merge_diff(original, result); + // differs from the RFC 7396, "c": null is not in the patch + // because neither the source nor the target have the "c" key + CHECK(patch == R"({"a":{"b":"d"}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 8") + { + json original = R"({"a":[{"b":"c"}]})"_json; + json result = R"({"a":[1]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[1]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 9") + { + json original = R"(["a","b"])"_json; + json result = R"(["c","d"])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(["c","d"])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 10") + { + json original = R"({"a":"b"})"_json; + json result = R"(["c"])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(["c"])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 11") + { + json original = R"({"a":"foo"})"_json; + json result = R"(null)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(null)"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 12") + { + json original = R"({"a":"foo"})"_json; + json result = R"("bar")"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"("bar")"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 13") + { + json original = R"({"e":null})"_json; + json result = R"({"e":null,"a":1})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 14") + { + json original = R"([1,2])"_json; + json result = R"({"a":"b"})"_json; + + auto patch = json::merge_diff(original, result); + // differs from the RFC 7396, "c": null is not in the patch + // because neither the source nor the target have the "c" key + CHECK(patch == R"({"a":"b"})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 15") + { + json original = R"({})"_json; + json result = R"({"a":{"bb":{}}})"_json; + + auto patch = json::merge_diff(original, result); + // differs from the RFC 7396, "ccc": null is not in the patch + // because neither the source nor the target have the "ccc" key + CHECK(patch == R"({"a":{"bb":{}}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + } + } + + SECTION("null values") + { + SECTION("object with null value to object") + { + json original = R"({"a":null})"_json; + json result = R"({"a":{"b":"c"}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":"c"}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{"b":"c"}})"_json); + } + + SECTION("object to object with null value") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("primitive to object with null value") + { + json original = R"({"a":1})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("nested primitive to object with null value") + { + json original = R"({"a":{"b":1}})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("array value to object with null value") + { + json original = R"({"a":[1,2]})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("nested object to object with null value") + { + json original = R"({"a":{"b":{"c":"d"}}})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("nested primitive to object with null value with sibling") + { + json original = R"({"x":"y","a":{"b":"c"}})"_json; + json result = R"({"x":"y","a":{"b":null, "c":"d"}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null,"c":"d"}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"x":"y","a":{"c":"d"}})"_json); + } + + SECTION("empty object to object with null value") + { + json original = R"({})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("null to object with null value") + { + json original = R"(null)"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("array to object with null value") + { + json original = R"([])"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == R"({})"_json); + } + + SECTION("empty object to nested null value") + { + json original = R"({})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("object to nested null value") + { + json original = R"({"a": 1})"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("primitive to nested null value") + { + json original = R"(1)"_json; + json result = R"({"a":{"b":null}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{}})"_json); + } + + SECTION("object with null value unchanged") + { + json original = R"({"a":null})"_json; + json result = R"({"a":null})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object with null value to empty object") + { + json original = R"({"a":null})"_json; + json result = R"({})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":null})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object with null value to primitive") + { + json original = R"({"a":null})"_json; + json result = R"({"a":1})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("array to array with null value") + { + json original = R"({"a":[1]})"_json; + json result = R"({"a":[null]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[null]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("empty object to array with null value") + { + json original = R"({})"_json; + json result = R"({"a":[null]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[null]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("doubly nested primitive to null value") + { + json original = R"({"a":{"b":{"c":1}}})"_json; + json result = R"({"a":{"b":{"c":null}}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":{"c":null}}})"_json); + + original.merge_patch(patch); + CHECK(original == R"({"a":{"b":{}}})"_json); + } + } + + SECTION("no change") + { + SECTION("object") + { + json original = R"({"a":"b","b":"c"})"_json; + json result = R"({"a":"b","b":"c"})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("empty object") + { + json original = R"({})"_json; + json result = R"({})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("array") + { + json original = R"([1,2,3])"_json; + json result = R"([1,2,3])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(!patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("null") + { + json original = R"(null)"_json; + json result = R"(null)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("string") + { + json original = R"("ab")"_json; + json result = R"("ab")"_json; + + auto patch = json::merge_diff(original, result); + CHECK(!patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("number") + { + json original = R"(42)"_json; + json result = R"(42)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(!patch.empty()); + original.merge_patch(patch); + CHECK(original == result); + } + } + + SECTION("primitives") + { + SECTION("string") + { + json original = R"("a")"_json; + json result = R"("b")"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"("b")"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("number") + { + json original = R"(1)"_json; + json result = R"(2)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(2)"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("boolean") + { + json original = R"(false)"_json; + json result = R"(true)"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"(true)"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to primitive") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":1})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("primitive to object") + { + json original = R"({"a":1})"_json; + json result = R"({"a":{"b":"c"}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":"c"}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to empty string") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":""})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":""})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to zero") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":0})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":0})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to empty object") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":{}})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":{"b":null}})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + } + + SECTION("arrays") + { + SECTION("array to array") + { + json original = R"([1,2,3])"_json; + json result = R"([1,2,4])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"([1,2,4])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("array to empty array") + { + json original = R"([1,2,3])"_json; + json result = R"([])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"([])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("empty array to array") + { + json original = R"([])"_json; + json result = R"([1,2,3])"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"([1,2,3])"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("object to empty array") + { + json original = R"({"a":{"b":"c"}})"_json; + json result = R"({"a":[]})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":[]})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("same keys") + { + json original = R"(["a","b"])"_json; + json result = R"({"a":1,"b":2})"_json; + + auto patch = json::merge_diff(original, result); + CHECK(patch == R"({"a":1,"b":2})"_json); + + original.merge_patch(patch); + CHECK(original == result); + } + } + + SECTION("ordered_json") + { + SECTION("changed value keeps member order") + { + auto original = ordered_json::parse(R"({"c":1,"b":2,"a":3})"); + auto result = ordered_json::parse(R"({"c":1,"b":5,"a":3})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"b":5})"); + + original.merge_patch(patch); + CHECK(original == result); + CHECK(original.dump() == R"({"c":1,"b":5,"a":3})"); + } + + SECTION("added members follow target order") + { + auto original = ordered_json::parse(R"({"b":1})"); + auto result = ordered_json::parse(R"({"b":1,"z":2,"a":3})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"z":2,"a":3})"); + + original.merge_patch(patch); + CHECK(original == result); + CHECK(original.dump() == R"({"b":1,"z":2,"a":3})"); + } + + SECTION("removed members follow source order") + { + auto original = ordered_json::parse(R"({"c":1,"b":2,"a":3})"); + auto result = ordered_json::parse(R"({"b":2})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"c":null,"a":null})"); + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("nested objects keep member order") + { + auto original = ordered_json::parse(R"({"z":{"y":1,"x":2},"a":0})"); + auto result = ordered_json::parse(R"({"z":{"y":1,"x":3,"w":4},"a":0})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"z":{"x":3,"w":4}})"); + + original.merge_patch(patch); + CHECK(original == result); + CHECK(original.dump() == R"({"z":{"y":1,"x":3,"w":4},"a":0})"); + } + + SECTION("reordered members cannot be expressed") + { + auto original = ordered_json::parse(R"({"a":1,"b":2})"); + auto result = ordered_json::parse(R"({"b":2,"a":1})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.empty()); + + original.merge_patch(patch); + CHECK(original != result); + CHECK(original.dump() == R"({"a":1,"b":2})"); + } + + SECTION("member added before existing ones is appended") + { + auto original = ordered_json::parse(R"({"b":1})"); + auto result = ordered_json::parse(R"({"a":0,"b":1})"); + + auto patch = ordered_json::merge_diff(original, result); + CHECK(patch.dump() == R"({"a":0})"); + + original.merge_patch(patch); + CHECK(original != result); + CHECK(original.dump() == R"({"b":1,"a":0})"); + } + } +} + +TEST_CASE("JSON Merge Patch diff of deeply nested values") +{ + SECTION("the patch is the same past the descent bound") + { + // depths on either side of the nesting depth up to which merge_diff() + // recurses (detail::recursion_depth_limit(), 128); not every depth up + // to 300, to keep the test fast under Valgrind + std::vector depths; + for (std::size_t depth = 0; depth <= 16; ++depth) + { + depths.push_back(depth); + } + for (std::size_t depth = 120; depth <= 136; ++depth) + { + depths.push_back(depth); + } + depths.push_back(300); + + for (const auto depth : depths) + { + CAPTURE(depth) + for (int from = 0; from < 3; ++from) + { + for (int to = 0; to < 3; ++to) + { + CAPTURE(from) + CAPTURE(to) + const auto source = nested_objects(depth, from); + const auto target = nested_objects(depth, to); + const auto patch = json::merge_diff(source, target); + CHECK(patch == reference_merge_diff(source, target)); + CHECK(patch.empty() == (from == to)); + + // a merge patch cannot set a member to null, so it only + // reproduces a target without null members + if (to != 2) + { + json patched = source; + patched.merge_patch(patch); + CHECK(patched == target); + } + + // the members of the patch also come in the same order + const auto ordered_source = nested_objects(depth, from); + const auto ordered_target = nested_objects(depth, to); + CHECK(ordered_json::merge_diff(ordered_source, ordered_target).dump() == reference_merge_diff(ordered_source, ordered_target).dump()); + } + } + } + } + + SECTION("a difference only in the innermost value is a patch nested as deeply") + { + for (std::size_t depth = 0; depth <= 300; ++depth) + { + CAPTURE(depth) + json source = {{"v", 1}}; + json target = {{"v", 2}}; + json expected = {{"v", 2}}; + for (std::size_t i = 0; i < depth; ++i) + { + source = json{{"a", std::move(source)}, {"b", i}}; + target = json{{"a", std::move(target)}, {"b", i}}; + expected = json{{"a", std::move(expected)}}; + } + CHECK(json::merge_diff(source, target) == expected); + } + } + + SECTION("values nested too deeply for the call stack") + { + // recursing once per nesting level would exhaust the call stack here. + // The patch is walked rather than compared as a whole, to keep the + // test fast. + const std::size_t depth = 100000; + std::string opening; + for (std::size_t i = 0; i < depth; ++i) + { + opening += R"({"a":)"; + } + const std::string closing(depth, '}'); + const auto source = json::parse(opening + R"({"v":1,"w":0})" + closing); + + const auto patch = json::merge_diff(source, json::parse(opening + R"({"v":2,"w":0})" + closing)); + std::size_t members = 0; + const json* p = &patch; + for (std::size_t i = 0; i < depth; ++i) + { + members += p->size(); + p = &p->at("a"); + } + CHECK(members == depth); + CHECK(*p == json({{"v", 2}})); + + CHECK(json::merge_diff(source, json::parse(opening + R"({"v":1,"w":0})" + closing)).empty()); + } +}