diff --git a/develop/json.hpp b/develop/json.hpp index f4c139e47..77888236d 100644 --- a/develop/json.hpp +++ b/develop/json.hpp @@ -7397,6 +7397,7 @@ class basic_json diff for two JSON values.,diff} @sa @ref patch -- apply a JSON patch + @sa @ref merge_patch -- apply a JSON Merge Patch @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) @@ -7528,6 +7529,83 @@ class basic_json } /// @} + + //////////////////////////////// + // JSON Merge Patch functions // + //////////////////////////////// + + /// @name JSON Merge Patch functions + /// @{ + + /*! + @brief applies a JSON Merge Patch + + The merge patch format is primarily intended for use with the HTTP PATCH + method as a means of describing a set of modifications to a target + resource's content. This function applies a merge patch to the current + JSON value. + + The function implements the following algorithm from Section 2 of + [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396): + + ``` + define MergePatch(Target, Patch): + if Patch is an Object: + if Target is not an Object: + Target = {} // Ignore the contents and set it to an empty Object + for each Name/Value pair in Patch: + if Value is null: + if Name exists in Target: + remove the Name/Value pair from Target + else: + Target[Name] = MergePatch(Target[Name], Value) + return Target + else: + return Patch + ``` + + Thereby, `Target` is the current object; that is, the patch is applied to + the current value. + + @param[in] patch the patch to apply + + @complexity Linear in the lengths of @a patch. + + @liveexample{The following code shows how a JSON Merge Patch is applied to + a JSON document.,merge_patch} + + @sa @ref patch -- apply a JSON patch + @sa [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396) + + @since version 3.0.0 + */ + void merge_patch(const basic_json& patch) + { + if (patch.is_object()) + { + if (not is_object()) + { + *this = object(); + } + for (auto it = patch.begin(); it != patch.end(); ++it) + { + if (it.value().is_null()) + { + erase(it.key()); + } + else + { + operator[](it.key()).merge_patch(it.value()); + } + } + } + else + { + *this = patch; + } + } + + /// @} }; ////////////////// diff --git a/doc/examples/merge_patch.cpp b/doc/examples/merge_patch.cpp new file mode 100644 index 000000000..77a5f2a25 --- /dev/null +++ b/doc/examples/merge_patch.cpp @@ -0,0 +1,40 @@ +#include +#include "json.hpp" +#include // for std::setw + +using json = nlohmann::json; + +int main() +{ + // the original document + json document = R"({ + "title": "Goodbye!", + "author": { + "givenName": "John", + "familyName": "Doe" + }, + "tags": [ + "example", + "sample" + ], + "content": "This will be unchanged" + })"_json; + + // the patch + json patch = R"({ + "title": "Hello!", + "phoneNumber": "+01-123-456-7890", + "author": { + "familyName": null + }, + "tags": [ + "example" + ] + })"_json; + + // apply the patch + document.merge_patch(patch); + + // output original and patched document + std::cout << std::setw(4) << document << std::endl; +} diff --git a/doc/examples/merge_patch.link b/doc/examples/merge_patch.link new file mode 100644 index 000000000..7a6cbbace --- /dev/null +++ b/doc/examples/merge_patch.link @@ -0,0 +1 @@ +online \ No newline at end of file diff --git a/doc/examples/merge_patch.output b/doc/examples/merge_patch.output new file mode 100644 index 000000000..96adb7b3b --- /dev/null +++ b/doc/examples/merge_patch.output @@ -0,0 +1,11 @@ +{ + "author": { + "givenName": "John" + }, + "content": "This will be unchanged", + "phoneNumber": "+01-123-456-7890", + "tags": [ + "example" + ], + "title": "Hello!" +} diff --git a/src/json.hpp b/src/json.hpp index a80cc36aa..2abea0fd4 100644 --- a/src/json.hpp +++ b/src/json.hpp @@ -14685,6 +14685,7 @@ class basic_json diff for two JSON values.,diff} @sa @ref patch -- apply a JSON patch + @sa @ref merge_patch -- apply a JSON Merge Patch @sa [RFC 6902 (JSON Patch)](https://tools.ietf.org/html/rfc6902) @@ -14816,6 +14817,83 @@ class basic_json } /// @} + + //////////////////////////////// + // JSON Merge Patch functions // + //////////////////////////////// + + /// @name JSON Merge Patch functions + /// @{ + + /*! + @brief applies a JSON Merge Patch + + The merge patch format is primarily intended for use with the HTTP PATCH + method as a means of describing a set of modifications to a target + resource's content. This function applies a merge patch to the current + JSON value. + + The function implements the following algorithm from Section 2 of + [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396): + + ``` + define MergePatch(Target, Patch): + if Patch is an Object: + if Target is not an Object: + Target = {} // Ignore the contents and set it to an empty Object + for each Name/Value pair in Patch: + if Value is null: + if Name exists in Target: + remove the Name/Value pair from Target + else: + Target[Name] = MergePatch(Target[Name], Value) + return Target + else: + return Patch + ``` + + Thereby, `Target` is the current object; that is, the patch is applied to + the current value. + + @param[in] patch the patch to apply + + @complexity Linear in the lengths of @a patch. + + @liveexample{The following code shows how a JSON Merge Patch is applied to + a JSON document.,merge_patch} + + @sa @ref patch -- apply a JSON patch + @sa [RFC 7396 (JSON Merge Patch)](https://tools.ietf.org/html/rfc7396) + + @since version 3.0.0 + */ + void merge_patch(const basic_json& patch) + { + if (patch.is_object()) + { + if (not is_object()) + { + *this = object(); + } + for (auto it = patch.begin(); it != patch.end(); ++it) + { + if (it.value().is_null()) + { + erase(it.key()); + } + else + { + operator[](it.key()).merge_patch(it.value()); + } + } + } + else + { + *this = patch; + } + } + + /// @} }; ////////////////// diff --git a/test/Makefile b/test/Makefile index bd8b73f03..427db7944 100644 --- a/test/Makefile +++ b/test/Makefile @@ -28,6 +28,7 @@ SOURCES = src/unit.cpp \ src/unit-iterator_wrapper.cpp \ src/unit-iterators1.cpp \ src/unit-iterators2.cpp \ + src/unit-merge_patch.cpp \ src/unit-json_patch.cpp \ src/unit-json_pointer.cpp \ src/unit-meta.cpp \ diff --git a/test/src/unit-merge_patch.cpp b/test/src/unit-merge_patch.cpp new file mode 100644 index 000000000..79bbaa2a3 --- /dev/null +++ b/test/src/unit-merge_patch.cpp @@ -0,0 +1,261 @@ +/* + __ _____ _____ _____ + __| | __| | | | JSON for Modern C++ (test suite) +| | |__ | | | | | | version 2.1.1 +|_____|_____|_____|_|___| https://github.com/nlohmann/json + +Licensed under the MIT License . +Copyright (c) 2013-2017 Niels Lohmann . + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +*/ + +#include "catch.hpp" + +#include "json.hpp" +using nlohmann::json; + +TEST_CASE("JSON Merge Patch") +{ + SECTION("examples from RFC 7396") + { + SECTION("Section 1") + { + json document = R"({ + "a": "b", + "c": { + "d": "e", + "f": "g" + } + })"_json; + + json patch = R"({ + "a": "z", + "c": { + "f": null + } + })"_json; + + json expected = R"({ + "a": "z", + "c": { + "d": "e" + } + })"_json; + + 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 patch = R"({ + "title": "Hello!", + "phoneNumber": "+01-123-456-7890", + "author": { + "familyName": null + }, + "tags": [ + "example" + ] + })"_json; + + json expected = R"({ + "title": "Hello!", + "author": { + "givenName": "John" + }, + "tags": [ + "example" + ], + "content": "This will be unchanged", + "phoneNumber": "+01-123-456-7890" + })"_json; + + document.merge_patch(patch); + CHECK(document == expected); + } + + SECTION("Appendix A") + { + SECTION("Example 1") + { + json original = R"({"a":"b"})"_json; + json patch = R"({"a":"c"})"_json; + json result = R"({"a":"c"})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 2") + { + json original = R"({"a":"b"})"_json; + json patch = R"({"b":"c"})"_json; + json result = R"({"a":"b", "b":"c"})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 3") + { + json original = R"({"a":"b"})"_json; + json patch = R"({"a":null})"_json; + json result = R"({})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 4") + { + json original = R"({"a":"b","b":"c"})"_json; + json patch = R"({"a":null})"_json; + json result = R"({"b":"c"})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 5") + { + json original = R"({"a":["b"]})"_json; + json patch = R"({"a":"c"})"_json; + json result = R"({"a":"c"})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 6") + { + json original = R"({"a":"c"})"_json; + json patch = R"({"a":["b"]})"_json; + json result = R"({"a":["b"]})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 7") + { + json original = R"({"a":{"b": "c"}})"_json; + json patch = R"({"a":{"b":"d","c":null}})"_json; + json result = R"({"a": {"b": "d"}})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 8") + { + json original = R"({"a":[{"b":"c"}]})"_json; + json patch = R"({"a":[1]})"_json; + json result = R"({"a":[1]})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 9") + { + json original = R"(["a","b"])"_json; + json patch = R"(["c","d"])"_json; + json result = R"(["c","d"])"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 10") + { + json original = R"({"a":"b"})"_json; + json patch = R"(["c"])"_json; + json result = R"(["c"])"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 11") + { + json original = R"({"a":"foo"})"_json; + json patch = R"(null)"_json; + json result = R"(null)"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 12") + { + json original = R"({"a":"foo"})"_json; + json patch = R"("bar")"_json; + json result = R"("bar")"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 13") + { + json original = R"({"e":null})"_json; + json patch = R"({"a":1})"_json; + json result = R"({"e":null,"a":1})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 14") + { + json original = R"([1,2])"_json; + json patch = R"({"a":"b","c":null})"_json; + json result = R"({"a":"b"})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + + SECTION("Example 15") + { + json original = R"({})"_json; + json patch = R"({"a":{"bb":{"ccc":null}}})"_json; + json result = R"({"a":{"bb":{}}})"_json; + + original.merge_patch(patch); + CHECK(original == result); + } + } + } +}