Commit 70fc5835 authored by Niels's avatar Niels

started implementing JSON Patch (RFC 6902)

parent 3ca1bfdd
......@@ -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;
}
};
......
......@@ -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;
}
};
......
......@@ -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")
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment