started implementing JSON Patch (RFC 6902)
This commit is contained in:
parent
3ca1bfdd9d
commit
70fc5835cb
3 changed files with 416 additions and 0 deletions
95
src/json.hpp
95
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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
|
226
test/unit.cpp
226
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")
|
||||
|
|
Loading…
Reference in a new issue