From 70fc5835cb4758b73fdab800380a7e3246ff79f7 Mon Sep 17 00:00:00 2001
From: Niels <niels.lohmann@gmail.com>
Date: Mon, 18 Apr 2016 22:41:36 +0200
Subject: [PATCH] started implementing JSON Patch (RFC 6902)

---
 src/json.hpp      |  95 +++++++++++++++++++
 src/json.hpp.re2c |  95 +++++++++++++++++++
 test/unit.cpp     | 226 ++++++++++++++++++++++++++++++++++++++++++++++
 3 files changed, 416 insertions(+)

diff --git a/src/json.hpp b/src/json.hpp
index 57a8f4c7..421b9995 100644
--- a/src/json.hpp
+++ b/src/json.hpp
@@ -9492,6 +9492,101 @@ basic_json_parser_63:
     }
 
     /// @}
+
+    /*!
+    @brief applies a JSON patch
+
+    @param[in] patch  JSON patch document
+    @return patched document
+
+    @note The original JSON value is not changed; that is, the patch is
+          applied to a copy of the value.
+
+    @sa [RFC 6902](https://tools.ietf.org/html/rfc6902)
+    */
+    basic_json apply_patch(const basic_json& patch) const
+    {
+        basic_json result = *this;
+
+        if (not patch.is_array())
+        {
+            // a JSON patch must be an array of objects
+            throw std::domain_error("JSON patch must be an array of objects");
+        }
+
+        for (const auto& val : patch)
+        {
+            if (not val.is_object())
+            {
+                throw std::domain_error("JSON patch must be an array of objects");
+            }
+
+            // collect members
+            const auto it_op = val.m_value.object->find("op");
+            const auto it_path = val.m_value.object->find("path");
+            const auto it_value = val.m_value.object->find("value");
+
+            if (it_op == val.m_value.object->end() or not it_op->second.is_string())
+            {
+                throw std::domain_error("operation must have a string 'op' member");
+            }
+
+            if (it_path == val.m_value.object->end() or not it_op->second.is_string())
+            {
+                throw std::domain_error("operation must have a string 'path' member");
+            }
+
+            const std::string op = it_op->second;
+            const std::string path = it_path->second;
+            const json_pointer ptr(path);
+
+            if (op == "add")
+            {
+                if (it_value == val.m_value.object->end())
+                {
+                    throw std::domain_error("'add' operation must have member 'value'");
+                }
+
+                result[ptr] = it_value->second;
+            }
+            else if (op == "remove")
+            {
+            }
+            else if (op == "replace")
+            {
+                if (it_value == val.m_value.object->end())
+                {
+                    throw std::domain_error("'replace' operation must have member 'value'");
+                }
+            }
+            else if (op == "move")
+            {
+            }
+            else if (op == "copy")
+            {
+            }
+            else if (op == "test")
+            {
+                if (it_value == val.m_value.object->end())
+                {
+                    throw std::domain_error("'test' operation must have member 'value'");
+                }
+
+                if (result.at(ptr) != it_value->second)
+                {
+                    throw std::domain_error("unsuccessful: " + val.dump());
+                }
+            }
+            else
+            {
+                // op must be "add", "remove", "replace", "move",
+                // "copy", or "test"
+                throw std::domain_error("operation value '" + op + "' is invalid");
+            }
+        }
+
+        return result;
+    }
 };
 
 
diff --git a/src/json.hpp.re2c b/src/json.hpp.re2c
index cdd96ee9..3f839737 100644
--- a/src/json.hpp.re2c
+++ b/src/json.hpp.re2c
@@ -8802,6 +8802,101 @@ class basic_json
     }
 
     /// @}
+
+    /*!
+    @brief applies a JSON patch
+
+    @param[in] patch  JSON patch document
+    @return patched document
+
+    @note The original JSON value is not changed; that is, the patch is
+          applied to a copy of the value.
+
+    @sa [RFC 6902](https://tools.ietf.org/html/rfc6902)
+    */
+    basic_json apply_patch(const basic_json& patch) const
+    {
+        basic_json result = *this;
+
+        if (not patch.is_array())
+        {
+            // a JSON patch must be an array of objects
+            throw std::domain_error("JSON patch must be an array of objects");
+        }
+
+        for (const auto& val : patch)
+        {
+            if (not val.is_object())
+            {
+                throw std::domain_error("JSON patch must be an array of objects");
+            }
+
+            // collect members
+            const auto it_op = val.m_value.object->find("op");
+            const auto it_path = val.m_value.object->find("path");
+            const auto it_value = val.m_value.object->find("value");
+
+            if (it_op == val.m_value.object->end() or not it_op->second.is_string())
+            {
+                throw std::domain_error("operation must have a string 'op' member");
+            }
+
+            if (it_path == val.m_value.object->end() or not it_op->second.is_string())
+            {
+                throw std::domain_error("operation must have a string 'path' member");
+            }
+
+            const std::string op = it_op->second;
+            const std::string path = it_path->second;
+            const json_pointer ptr(path);
+
+            if (op == "add")
+            {
+                if (it_value == val.m_value.object->end())
+                {
+                    throw std::domain_error("'add' operation must have member 'value'");
+                }
+
+                result[ptr] = it_value->second;
+            }
+            else if (op == "remove")
+            {
+            }
+            else if (op == "replace")
+            {
+                if (it_value == val.m_value.object->end())
+                {
+                    throw std::domain_error("'replace' operation must have member 'value'");
+                }
+            }
+            else if (op == "move")
+            {
+            }
+            else if (op == "copy")
+            {
+            }
+            else if (op == "test")
+            {
+                if (it_value == val.m_value.object->end())
+                {
+                    throw std::domain_error("'test' operation must have member 'value'");
+                }
+
+                if (result.at(ptr) != it_value->second)
+                {
+                    throw std::domain_error("unsuccessful: " + val.dump());
+                }
+            }
+            else
+            {
+                // op must be "add", "remove", "replace", "move",
+                // "copy", or "test"
+                throw std::domain_error("operation value '" + op + "' is invalid");
+            }
+        }
+
+        return result;
+    }
 };
 
 
diff --git a/test/unit.cpp b/test/unit.cpp
index 2666e111..7a91efd7 100644
--- a/test/unit.cpp
+++ b/test/unit.cpp
@@ -12391,6 +12391,232 @@ TEST_CASE("JSON pointers")
     }
 }
 
+TEST_CASE("JSON patch")
+{
+    SECTION("examples from RFC 6902")
+    {
+        SECTION("example A.1 - Adding an Object Member")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                { "foo": "bar"}
+            )"_json;
+
+            // A JSON Patch document:
+            json patch = R"(
+                [
+                    { "op": "add", "path": "/baz", "value": "qux" }
+                ]
+            )"_json;
+
+            // The resulting JSON document:
+            json expected = R"(
+                {
+                    "baz": "qux",
+                    "foo": "bar"
+                }
+            )"_json;
+
+            // check if patched value is as expected
+            CHECK(doc.apply_patch(patch) == expected);
+        }
+
+        SECTION("example A.8 - Testing a Value: Success")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                {
+                     "baz": "qux",
+                     "foo": [ "a", 2, "c" ]
+                }
+            )"_json;
+
+            // A JSON Patch document that will result in successful evaluation:
+            json patch = R"(
+                [
+                    { "op": "test", "path": "/baz", "value": "qux" },
+                    { "op": "test", "path": "/foo/1", "value": 2 }
+                ]
+            )"_json;
+
+            // check if evaluation does not throw
+            CHECK_NOTHROW(doc.apply_patch(patch));
+            // check if patched document is unchanged
+            CHECK(doc.apply_patch(patch) == doc);
+        }
+
+        SECTION("example A.9 - Testing a Value: Error")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                { "baz": "qux" }
+            )"_json;
+
+            // A JSON Patch document that will result in an error condition:
+            json patch = R"(
+                [
+                    { "op": "test", "path": "/baz", "value": "bar" }
+                ]
+            )"_json;
+
+            // check that evaluation throws
+            CHECK_THROWS_AS(doc.apply_patch(patch), std::domain_error);
+            CHECK_THROWS_WITH(doc.apply_patch(patch), "unsuccessful: " + patch[0].dump());
+        }
+
+        SECTION("example A.10 - Adding a Nested Member Object")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                { "foo": "bar" }
+            )"_json;
+
+            // A JSON Patch document:
+            json patch = R"(
+                [
+                    { "op": "add", "path": "/child", "value": { "grandchild": { } } }
+                ]
+            )"_json;
+
+            // The resulting JSON document:
+            json expected = R"(
+            {
+                "foo": "bar",
+                "child": {
+                    "grandchild": {
+                    }
+                }
+            }
+            )"_json;
+
+            // check if patched value is as expected
+            CHECK(doc.apply_patch(patch) == expected);
+        }
+
+        SECTION("example A.11 - Ignoring Unrecognized Elements")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                { "foo": "bar" }
+            )"_json;
+
+            // A JSON Patch document:
+            json patch = R"(
+                [
+                    { "op": "add", "path": "/baz", "value": "qux", "xyz": 123 }
+                ]
+            )"_json;
+
+            json expected = R"(
+                {
+                    "foo": "bar",
+                    "baz": "qux"
+                } 
+            )"_json;
+
+            // check if patched value is as expected
+            CHECK(doc.apply_patch(patch) == expected);
+        }
+
+        SECTION("example A.12 - Adding to a Nonexistent Target")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                { "foo": "bar" }
+            )"_json;
+
+            // A JSON Patch document:
+            json patch = R"(
+                [
+                    { "op": "add", "path": "/baz/bat", "value": "qux" }
+                ]
+            )"_json;
+
+            // This JSON Patch document, applied to the target JSON document
+            // above, would result in an error (therefore, it would not be
+            // applied), because the "add" operation's target location that
+            // references neither the root of the document, nor a member of
+            // an existing object, nor a member of an existing array.
+
+            CHECK_THROWS_AS(doc.apply_patch(patch), std::out_of_range);
+            CHECK_THROWS_WITH(doc.apply_patch(patch), "unresolved reference token 'bat'");
+        }
+
+        SECTION("example A.14 - Escape Ordering")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                {
+                    "/": 9,
+                    "~1": 10
+                }
+            )"_json;
+
+            // A JSON Patch document:
+            json patch = R"(
+                [
+                    {"op": "test", "path": "/~01", "value": 10}
+                ]
+            )"_json;
+
+            json expected = R"(
+                {
+                    "/": 9,
+                    "~1": 10
+                } 
+            )"_json;
+
+            // check if patched value is as expected
+            CHECK(doc.apply_patch(patch) == expected);
+        }
+
+        SECTION("example A.15 - Comparing Strings and Numbers")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                {
+                    "/": 9,
+                    "~1": 10
+                } 
+            )"_json;
+
+            // A JSON Patch document that will result in an error condition:
+            json patch = R"(
+                [
+                    {"op": "test", "path": "/~01", "value": "10"}
+                ]
+            )"_json;
+
+            // check that evaluation throws
+            CHECK_THROWS_AS(doc.apply_patch(patch), std::domain_error);
+            CHECK_THROWS_WITH(doc.apply_patch(patch), "unsuccessful: " + patch[0].dump());
+        }
+
+        SECTION("example A.16 - Adding an Array Value")
+        {
+            // An example target JSON document:
+            json doc = R"(
+                { "foo": ["bar"] }
+            )"_json;
+
+            // A JSON Patch document:
+            json patch = R"(
+                [
+                    { "op": "add", "path": "/foo/-", "value": ["abc", "def"] }
+                ]
+            )"_json;
+
+            // The resulting JSON document:
+            json expected = R"(
+                { "foo": ["bar", ["abc", "def"]] }
+            )"_json;
+
+            // check if patched value is as expected
+            CHECK(doc.apply_patch(patch) == expected);
+        }
+    }
+}
+
 TEST_CASE("regression tests")
 {
     SECTION("issue #60 - Double quotation mark is not parsed correctly")