Commit e572a62c authored by Petr Lapukhov's avatar Petr Lapukhov Committed by Facebook Github Bot

JSON patch application method

Summary:
Mutate supplied object in place in accordance with patch instructions. If patch fails, object may be left in partially modified state.

Notice that patch application is placed in json_patch as we rely on dynamic object to store the patch itself.

Reviewed By: yfeldblum

Differential Revision: D10427475

fbshipit-source-id: 9ebc927c31fdf7434e69cb5c8df01c6979cab6e0
parent 5824119b
...@@ -16,6 +16,8 @@ ...@@ -16,6 +16,8 @@
#include <folly/json_patch.h> #include <folly/json_patch.h>
#include <folly/container/Enumerate.h>
namespace { namespace {
using folly::StringPiece; using folly::StringPiece;
// JSON patch operation names // JSON patch operation names
...@@ -152,4 +154,170 @@ std::vector<json_patch::patch_operation> const& json_patch::ops() const { ...@@ -152,4 +154,170 @@ std::vector<json_patch::patch_operation> const& json_patch::ops() const {
return ops_; return ops_;
} }
namespace {
// clang-format off
Expected<Unit, json_patch::patch_application_error_code>
// clang-format on
do_remove(dynamic::resolved_json_pointer<dynamic>& ptr) {
using error_code = json_patch::patch_application_error_code;
if (!ptr.hasValue()) {
return folly::makeUnexpected(error_code::path_not_found);
}
auto parent = ptr->parent;
if (!parent) {
return folly::makeUnexpected(error_code::other);
}
if (parent->isObject()) {
parent->erase(ptr->parent_key);
return unit;
}
if (parent->isArray()) {
parent->erase(parent->begin() + ptr->parent_index);
return unit;
}
return folly::makeUnexpected(error_code::other);
}
// clang-format off
Expected<Unit, json_patch::patch_application_error_code>
// clang-format on
do_add(
dynamic::resolved_json_pointer<dynamic>& ptr,
const dynamic& value,
const std::string& last_token) {
using app_err_code = json_patch::patch_application_error_code;
using res_err_code = dynamic::json_pointer_resolution_error_code;
// element found: see if parent is object or array
if (ptr.hasValue()) {
// root element, or key in object - replace (per RFC)
if (ptr->parent == nullptr || ptr->parent->isObject()) {
*ptr->value = value;
}
// valid index in array: insert at index and shift right
if (ptr->parent && ptr->parent->isArray()) {
ptr->parent->insert(ptr->parent->begin() + ptr->parent_index, value);
}
} else {
// see if we can add value, based on pointer resolution state
switch (ptr.error().error_code) {
// key not found. can only happen in object - add new key-value
case res_err_code::key_not_found: {
DCHECK(ptr.error().context->isObject());
ptr.error().context->insert(last_token, value);
break;
}
// special '-' index in array - do append operation
case res_err_code::append_requested: {
DCHECK(ptr.error().context->isArray());
ptr.error().context->push_back(value);
break;
}
default:
return folly::makeUnexpected(app_err_code::other);
}
}
return unit;
}
} // namespace
// clang-format off
Expected<Unit, json_patch::patch_application_error>
// clang-format on
json_patch::apply(dynamic& obj) {
using op_code = patch_operation_code;
using error_code = patch_application_error_code;
using error = patch_application_error;
for (auto&& it : enumerate(ops_)) {
auto const index = it.index;
auto const& op = *it;
auto resolved_path = obj.try_get_ptr(op.path);
switch (op.op_code) {
case op_code::test:
if (!resolved_path.hasValue()) {
return folly::makeUnexpected(
error{error_code::path_not_found, index});
}
if (*resolved_path->value != *op.value) {
return folly::makeUnexpected(error{error_code::test_failed, index});
}
break;
case op_code::remove: {
auto ret = do_remove(resolved_path);
if (ret.hasError()) {
return makeUnexpected(error{ret.error(), index});
}
break;
}
case op_code::add: {
DCHECK(op.value.hasValue());
auto ret = do_add(resolved_path, *op.value, op.path.tokens().back());
if (ret.hasError()) {
return makeUnexpected(error{ret.error(), index});
}
break;
}
case op_code::replace: {
if (resolved_path.hasValue()) {
*resolved_path->value = *op.value;
} else {
return folly::makeUnexpected(
error{error_code::path_not_found, index});
}
break;
}
case op_code::move: {
DCHECK(op.from.hasValue());
auto resolved_from = obj.try_get_ptr(*op.from);
if (!resolved_from.hasValue()) {
return makeUnexpected(error{error_code::from_not_found, index});
}
{
auto ret = do_add(
resolved_path, *resolved_from->value, op.path.tokens().back());
if (ret.hasError()) {
return makeUnexpected(error{ret.error(), index});
}
}
{
auto ret = do_remove(resolved_from);
if (ret.hasError()) {
return makeUnexpected(error{ret.error(), index});
}
}
break;
}
case op_code::copy: {
DCHECK(op.from.hasValue());
auto const resolved_from = obj.try_get_ptr(*op.from);
if (!resolved_from.hasValue()) {
return makeUnexpected(error{error_code::from_not_found, index});
}
{
DCHECK(!op.path.tokens().empty());
auto ret = do_add(
resolved_path, *resolved_from->value, op.path.tokens().back());
if (ret.hasError()) {
return makeUnexpected(error{ret.error(), index});
}
}
break;
}
case op_code::invalid: {
DCHECK(false);
return makeUnexpected(error{error_code::other, index});
}
}
}
return unit;
}
} // namespace folly } // namespace folly
...@@ -97,6 +97,28 @@ class json_patch { ...@@ -97,6 +97,28 @@ class json_patch {
std::vector<patch_operation> const& ops() const; std::vector<patch_operation> const& ops() const;
enum class patch_application_error_code : uint8_t {
other,
// "from" pointer did not resolve
from_not_found,
// "path" pointer did not resolve
path_not_found,
// "test" condition failed
test_failed,
};
struct patch_application_error {
patch_application_error_code error_code{};
// index of the patch element (in array) that caused error
size_t index{};
};
/*
* Mutate supplied object in accordance with patch operations. Leaves
* object in partially modified state if one of the operations fails.
*/
Expected<Unit, patch_application_error> apply(folly::dynamic& obj);
private: private:
std::vector<patch_operation> ops_; std::vector<patch_operation> ops_;
}; };
......
...@@ -197,3 +197,165 @@ TEST_F(JsonPatchTest, InvalidPatches) { ...@@ -197,3 +197,165 @@ TEST_F(JsonPatchTest, InvalidPatches) {
EXPECT_EQ(&jsonObj[2], err.obj); EXPECT_EQ(&jsonObj[2], err.obj);
EXPECT_EQ(jsonObj[2], *err.obj); EXPECT_EQ(jsonObj[2], *err.obj);
} }
TEST_F(JsonPatchTest, SuccessfulPatchApplication) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" },
{ "op": "add", "path": "/a/b/c", "value": [ "foo", "bar" ] },
{ "op": "replace", "path": "/a/b/c", "value": 42 },
{ "op": "test", "path": "/a/b/c", "value": 42 },
{ "op": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" },
{ "op": "add", "path": "/a/b/c", "value": [1, 2, 3] },
{ "op": "add", "path": "/a/b/c/1", "value": 100 }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::object("c", "foo")));
auto res = patch.apply(objToMutate);
EXPECT_FALSE(res.hasError()) << (int)res.error().error_code;
EXPECT_EQ(42, objToMutate["a"]["b"]["d"].asInt());
EXPECT_EQ(42, objToMutate["a"]["b"]["e"].asInt());
EXPECT_EQ(100, objToMutate["a"]["b"]["c"][1].asInt());
EXPECT_EQ(1, objToMutate["a"]["b"]["c"][0].asInt());
}
TEST_F(JsonPatchTest, TestOpFailure) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "test", "path": "/a/b/c", "value": "foo" },
{ "op": "remove", "path": "/a/b/c" }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::object("c", "bar")));
auto res = patch.apply(objToMutate);
EXPECT_TRUE(res.hasError());
EXPECT_EQ(
json_patch::patch_application_error_code::test_failed,
res.error().error_code);
EXPECT_EQ("bar", objToMutate["a"]["b"]["c"].asString());
}
TEST_F(JsonPatchTest, PathNotFound) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "remove", "path": "/a/b/d" }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::object("c", "foo")));
auto res = patch.apply(objToMutate);
EXPECT_TRUE(res.hasError());
EXPECT_EQ(
json_patch::patch_application_error_code::path_not_found,
res.error().error_code);
EXPECT_EQ("foo", objToMutate["a"]["b"]["c"].asString());
}
TEST_F(JsonPatchTest, FromNotFound) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "copy", "from": "/a/c/b", "path": "/a/b/d" }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::object("c", "foo")));
auto res = patch.apply(objToMutate);
EXPECT_TRUE(res.hasError());
EXPECT_EQ(
json_patch::patch_application_error_code::from_not_found,
res.error().error_code);
EXPECT_THROW(objToMutate["a"]["b"].at("d"), std::out_of_range);
}
TEST_F(JsonPatchTest, RemoveRootObject) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "remove", "path": "" }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::object("c", "foo")));
auto res = patch.apply(objToMutate);
EXPECT_TRUE(res.hasError());
EXPECT_EQ(
json_patch::patch_application_error_code::other, res.error().error_code);
EXPECT_EQ("foo", objToMutate["a"]["b"]["c"].asString());
}
TEST_F(JsonPatchTest, AddWithAppend) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "add", "path": "/a/b/-", "value": 100 }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::array(1, 2, 3, 4)));
auto res = patch.apply(objToMutate);
EXPECT_TRUE(res.hasValue());
EXPECT_EQ(100, objToMutate["a"]["b"][4].asInt());
}
TEST_F(JsonPatchTest, RemoveWithMinus) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "remove", "path": "/a/b/-" }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::array(1, 2, 3, 4)));
auto res = patch.apply(objToMutate);
EXPECT_TRUE(res.hasError());
EXPECT_EQ(
json_patch::patch_application_error_code::path_not_found,
res.error().error_code);
}
TEST_F(JsonPatchTest, FailedOpIndex) {
constexpr folly::StringPiece jsonPatchStr = R"(
[
{ "op": "test", "path": "/a/b/0", "value": 1 },
{ "op": "test", "path": "/a/b/0", "value": 2 }
])";
auto patch = json_patch::try_parse(folly::parseJson(jsonPatchStr)).value();
dynamic objToMutate =
dynamic::object("a", dynamic::object("b", dynamic::array(1, 2, 3, 4)));
auto res = patch.apply(objToMutate);
EXPECT_TRUE(res.hasError());
EXPECT_EQ(
json_patch::patch_application_error_code::test_failed,
res.error().error_code);
EXPECT_EQ(1, res.error().index);
}
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