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")
|
TEST_CASE("regression tests")
|
||||||
{
|
{
|
||||||
SECTION("issue #60 - Double quotation mark is not parsed correctly")
|
SECTION("issue #60 - Double quotation mark is not parsed correctly")
|
||||||
|
|
Loading…
Reference in a new issue