From 70fc5835cb4758b73fdab800380a7e3246ff79f7 Mon Sep 17 00:00:00 2001 From: Niels 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")