mirror of
https://github.com/nlohmann/json.git
synced 2026-10-13 10:14:53 +00:00
* support JSON Merge Patch (RFC 7396) diff creation The JSON merge patch document format describes the set of modifications to a resource's content, that more closely mimics the syntax of the resource being modified. However, and in contrast to JSON Patch (RFC 6902), a JSON Merge Patch cannot express certain modifications, e.g., changing an array element at a specific index, or setting a specific object value to null. The null value in a JSON Merge Patch is used to remove the key from the object. The diff algorithm is not part of the RFC 7396, but it was tested against all examples provided, plus additional cases on how null values are handled. Signed-off-by: Luís Murta <luis@murta.dev> * improve algorithm efficiency Signed-off-by: Luís Murta <luis@murta.dev> * fix inconsistent nulls and empty arrays Also add CHECKs for the patch, documentation and run `make amalgamate`. Signed-off-by: Luís Murta <luis@murta.dev> * documentation cleanups Signed-off-by: Luís Murta <luis@murta.dev> * add an iterative fallback to merge_diff Bound the recursion depth like diff() and merge_patch(), and test values nested past the bound. Signed-off-by: Luís Murta <luis@murta.dev> * address the remaining review comments Signed-off-by: Luís Murta <luis@murta.dev> * move multi-line raw strings out of CHECK macros GCC 4.8/5, ICPC and NVHPC mis-tokenize multi-line raw string literals inside macro arguments, breaking the build or the parsed JSON. Signed-off-by: Luís Murta <luis@murta.dev> --------- Signed-off-by: Luís Murta <luis@murta.dev>
1025 lines
31 KiB
C++
1025 lines
31 KiB
C++
// __ _____ _____ _____
|
|
// __| | __| | | | JSON for Modern C++ (supporting code)
|
|
// | | |__ | | | | | | version 3.12.0
|
|
// |_____|_____|_____|_|___| https://github.com/nlohmann/json
|
|
//
|
|
// SPDX-FileCopyrightText: 2013-2026 Niels Lohmann <https://nlohmann.me>
|
|
// SPDX-License-Identifier: MIT
|
|
|
|
#include "doctest_compatibility.h"
|
|
|
|
#include <nlohmann/json.hpp>
|
|
using nlohmann::json;
|
|
using nlohmann::ordered_json;
|
|
#ifdef JSON_TEST_NO_GLOBAL_UDLS
|
|
using namespace nlohmann::literals; // NOLINT(google-build-using-namespace)
|
|
#endif
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
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<typename BasicJsonType>
|
|
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<typename BasicJsonType>
|
|
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<std::size_t>(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<std::size_t> 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<json>(depth, from);
|
|
const auto target = nested_objects<json>(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<ordered_json>(depth, from);
|
|
const auto ordered_target = nested_objects<ordered_json>(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());
|
|
}
|
|
}
|