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

RFC 6902 - JSON Patch representation

Summary: Consume `folly::dynamic` and if it matches JSON patch format, build a sequence of patch operations. Application is to be done separately, e.g. by a method of `folly::dynamic` by walking the operations vector (obtained after parsing the patch) and applying operations to the document. This mimics the approach we used previously with `folly::json_pointer`.

Reviewed By: yfeldblum

Differential Revision: D10017234

fbshipit-source-id: 1d0b9f0967e528b81abf7998c88abdaf70a2fce8
parent be2320bf
......@@ -668,6 +668,7 @@ if (BUILD_TESTS)
# MSVC Preprocessor stringizing raw string literals bug
#TEST json_test SOURCES JsonTest.cpp
TEST json_pointer_test SOURCES json_pointer_test.cpp
TEST json_patch_test SOURCES json_patch_test.cpp
TEST json_other_test
CONTENT_DIR json_test_data/
SOURCES
......
/*
* Copyright 2011-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/json_patch.h>
namespace {
using folly::StringPiece;
// JSON patch operation names
constexpr StringPiece kOperationTest = "test";
constexpr StringPiece kOperationRemove = "remove";
constexpr StringPiece kOperationAdd = "add";
constexpr StringPiece kOperationReplace = "replace";
constexpr StringPiece kOperationMove = "move";
constexpr StringPiece kOperationCopy = "copy";
// field tags in JSON patch
constexpr StringPiece kOpTag = "op";
constexpr StringPiece kValueTag = "value";
constexpr StringPiece kPathTag = "path";
constexpr StringPiece kFromTag = "from";
} // namespace
namespace folly {
// static
Expected<json_patch, json_patch::parse_error> json_patch::try_parse(
dynamic const& obj) noexcept {
using err_code = parse_error_code;
json_patch patch;
if (!obj.isArray()) {
return makeUnexpected(parse_error{err_code::invalid_shape, &obj});
}
for (auto const& elem : obj) {
if (!elem.isObject()) {
return makeUnexpected(parse_error{err_code::invalid_shape, &elem});
}
auto const* op_ptr = elem.get_ptr(kOpTag);
if (!op_ptr) {
return makeUnexpected(parse_error{err_code::missing_op, &elem});
}
if (!op_ptr->isString()) {
return makeUnexpected(parse_error{err_code::malformed_op, &elem});
}
auto const op_str = op_ptr->asString();
patch_operation op;
// extract 'from' attribute
{
auto const* from_ptr = elem.get_ptr(kFromTag);
if (from_ptr) {
if (!from_ptr->isString()) {
return makeUnexpected(parse_error{err_code::invalid_shape, &elem});
}
auto json_ptr = json_pointer::try_parse(from_ptr->asString());
if (!json_ptr.hasValue()) {
return makeUnexpected(
parse_error{err_code::malformed_from_attr, &elem});
}
op.from = json_ptr.value();
}
}
// extract 'path' attribute
{
auto const* path_ptr = elem.get_ptr(kPathTag);
if (!path_ptr) {
return makeUnexpected(parse_error{err_code::missing_path_attr, &elem});
}
if (!path_ptr->isString()) {
return makeUnexpected(
parse_error{err_code::malformed_path_attr, &elem});
}
auto const json_ptr = json_pointer::try_parse(path_ptr->asString());
if (!json_ptr.hasValue()) {
return makeUnexpected(
parse_error{err_code::malformed_path_attr, &elem});
}
op.path = json_ptr.value();
}
// extract 'value' attribute
{
auto const* val_ptr = elem.get_ptr(kValueTag);
if (val_ptr) {
op.value = *val_ptr;
}
}
// check mandatory attributes - different per operation
// NOTE: per RFC, the surplus attributes (e.g. 'from' with 'add')
// should be simply ignored
using op_code = patch_operation_code;
if (op_str == kOperationTest) {
if (!op.value) {
return makeUnexpected(parse_error{err_code::missing_value_attr, &elem});
}
op.op_code = op_code::test;
} else if (op_str == kOperationRemove) {
op.op_code = op_code::remove;
} else if (op_str == kOperationAdd) {
if (!op.value) {
return makeUnexpected(parse_error{err_code::missing_value_attr, &elem});
}
op.op_code = op_code::add;
} else if (op_str == kOperationReplace) {
if (!op.value) {
return makeUnexpected(parse_error{err_code::missing_value_attr, &elem});
}
op.op_code = op_code::replace;
} else if (op_str == kOperationMove) {
if (!op.from) {
return makeUnexpected(parse_error{err_code::missing_from_attr, &elem});
}
// is from a proper prefix to path?
if (op.from->is_prefix_of(op.path)) {
return makeUnexpected(
parse_error{err_code::overlapping_pointers, &elem});
}
op.op_code = op_code::move;
} else if (op_str == kOperationCopy) {
if (!op.from) {
return makeUnexpected(parse_error{err_code::missing_from_attr, &elem});
}
op.op_code = op_code::copy;
}
if (op.op_code != op_code::invalid) {
patch.ops_.emplace_back(std::move(op));
} else {
return makeUnexpected(parse_error{err_code::unknown_op, &elem});
}
}
return patch;
}
std::vector<json_patch::patch_operation> const& json_patch::ops() const {
return ops_;
}
} // namespace folly
/*
* Copyright 2011-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#pragma once
#include <folly/Expected.h>
#include <folly/Optional.h>
#include <folly/dynamic.h>
#include <folly/json_pointer.h>
namespace folly {
/*
* json_patch
*
* As described in RFC 6902 "JSON Patch".
*
* Implements parsing. Application over data structures must be
* implemented separately.
*/
class json_patch {
public:
enum class parse_error_code : uint8_t {
undefined,
invalid_shape,
missing_op,
unknown_op,
malformed_op,
missing_path_attr,
malformed_path_attr,
missing_from_attr,
malformed_from_attr,
missing_value_attr,
overlapping_pointers,
};
/*
* If parsing JSON patch object fails we return err code along with
* pointer to part of JSON document that we could not parse
*/
struct parse_error {
// one of the above error codes
parse_error_code error_code{parse_error_code::undefined};
// pointer to object that caused the error
dynamic const* obj{};
};
enum class patch_operation_code : uint8_t {
invalid = 0,
test,
remove,
add,
replace,
move,
copy,
};
/*
* Single JSON patch operation. Argument may vary based on op type
*/
struct patch_operation {
patch_operation_code op_code{patch_operation_code::invalid};
json_pointer path;
Optional<json_pointer> from;
Optional<dynamic> value;
friend bool operator==(
patch_operation const& lhs,
patch_operation const& rhs) {
return lhs.op_code == rhs.op_code && lhs.path == rhs.path &&
lhs.from == rhs.from && lhs.value == rhs.value;
}
friend bool operator!=(
patch_operation const& lhs,
patch_operation const& rhs) {
return !(lhs == rhs);
}
};
json_patch() = default;
~json_patch() = default;
static folly::Expected<json_patch, parse_error> try_parse(
dynamic const& obj) noexcept;
std::vector<patch_operation> const& ops() const;
private:
std::vector<patch_operation> ops_;
};
} // namespace folly
......@@ -63,6 +63,16 @@ json_pointer json_pointer::parse(StringPiece const str) {
}
}
bool json_pointer::is_prefix_of(json_pointer const& other) const noexcept {
auto const& other_tokens = other.tokens();
if (tokens_.size() > other_tokens.size()) {
return false;
}
auto const other_begin = other_tokens.cbegin();
auto const other_end = other_tokens.cbegin() + tokens_.size();
return std::equal(tokens_.cbegin(), tokens_.cend(), other_begin, other_end);
}
std::vector<std::string> const& json_pointer::tokens() const {
return tokens_;
}
......
......@@ -54,12 +54,25 @@ class json_pointer {
static json_pointer parse(StringPiece const str);
/*
* Return true if this pointer is proper to prefix to another pointer
*/
bool is_prefix_of(json_pointer const& other) const noexcept;
/*
* Get access to the parsed tokens for applications that want to traverse
* the pointer.
*/
std::vector<std::string> const& tokens() const;
friend bool operator==(json_pointer const& lhs, json_pointer const& rhs) {
return lhs.tokens_ == rhs.tokens_;
}
friend bool operator!=(json_pointer const& lhs, json_pointer const& rhs) {
return lhs.tokens_ != rhs.tokens_;
}
private:
explicit json_pointer(std::vector<std::string>) noexcept;
......
/*
* Copyright 2011-present Facebook, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
#include <folly/json.h>
#include <folly/json_patch.h>
#include <folly/json_pointer.h>
#include <folly/portability/GMock.h>
#include <folly/portability/GTest.h>
using folly::dynamic;
using folly::json_patch;
using folly::json_pointer;
using err_code = folly::json_patch::parse_error_code;
using op_code = folly::json_patch::patch_operation_code;
class JsonPatchTest : public ::testing::Test {};
TEST_F(JsonPatchTest, ValidPatch) {
// from RFC 6902
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": "move", "from": "/a/b/c", "path": "/a/b/d" },
{ "op": "copy", "from": "/a/b/d", "path": "/a/b/e" }
])";
auto const expected = std::vector<json_patch::patch_operation>{
{op_code::test,
json_pointer::parse("/a/b/c"),
folly::none,
dynamic("foo")},
{op_code::remove,
json_pointer::parse("/a/b/c"),
folly::none,
folly::none},
{op_code::add,
json_pointer::parse("/a/b/c"),
folly::none,
folly::parseJson(R"(["foo", "bar"])")},
{op_code::replace,
json_pointer::parse("/a/b/c"),
folly::none,
dynamic(42)},
{op_code::move,
json_pointer::parse("/a/b/d"),
json_pointer::parse("/a/b/c"),
folly::none},
{op_code::copy,
json_pointer::parse("/a/b/e"),
json_pointer::parse("/a/b/d"),
folly::none}};
auto const parsed =
json_patch::try_parse(folly::parseJson(jsonPatchStr)).value().ops();
EXPECT_EQ(expected, parsed);
}
TEST_F(JsonPatchTest, InvalidPatches) {
EXPECT_EQ(
err_code::invalid_shape,
json_patch::try_parse(dynamic::object()).error().error_code);
EXPECT_EQ(
err_code::invalid_shape,
json_patch::try_parse(dynamic::array(dynamic::array()))
.error()
.error_code);
EXPECT_EQ(
err_code::missing_op,
json_patch::try_parse(folly::parseJson(R"([{"path": "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::unknown_op,
json_patch::try_parse(
folly::parseJson(R"([{"op": "blah", "path": "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::malformed_op,
json_patch::try_parse(
folly::parseJson(R"([{"op": ["blah"], "path": "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::missing_path_attr,
json_patch::try_parse(folly::parseJson(R"([{"op": "test"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::malformed_path_attr,
json_patch::try_parse(
folly::parseJson(R"([{"op": "test", "path" : "a/z/x"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::malformed_path_attr,
json_patch::try_parse(
folly::parseJson(R"([{"op": "test", "path" : ["a/z/x"]}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::missing_from_attr,
json_patch::try_parse(
folly::parseJson(R"([{"op": "copy", "path" : "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::malformed_from_attr,
json_patch::try_parse(
folly::parseJson(
R"([{"op": "copy", "from" : "c/d/e", "path" : "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::overlapping_pointers,
json_patch::try_parse(
folly::parseJson(
R"([{"op": "move", "from" : "/a/b/c", "path" : "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::overlapping_pointers,
json_patch::try_parse(
folly::parseJson(
R"([{"op": "move", "from" : "/a/b/c", "path" : "/a/b/c/d"}])"))
.error()
.error_code);
// validate presence of mandatory per-operation attributes
EXPECT_EQ(
err_code::missing_value_attr,
json_patch::try_parse(
folly::parseJson(R"([{"op": "test", "path" : "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::missing_value_attr,
json_patch::try_parse(
folly::parseJson(R"([{"op": "replace", "path" : "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::missing_from_attr,
json_patch::try_parse(
folly::parseJson(R"([{"op": "move", "path" : "/a/b/c"}])"))
.error()
.error_code);
EXPECT_EQ(
err_code::missing_from_attr,
json_patch::try_parse(
folly::parseJson(R"([{"op": "copy", "path" : "/a/b/c"}])"))
.error()
.error_code);
// test the object reference in error: in patch below, 3rd entry is incorrect
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" }
])";
auto jsonObj = folly::parseJson(jsonPatchStr);
auto err = json_patch::try_parse(jsonObj).error();
EXPECT_EQ(err_code::missing_value_attr, err.error_code);
// the invalid entry - check pointers and values they point at
EXPECT_EQ(&jsonObj[2], err.obj);
EXPECT_EQ(jsonObj[2], *err.obj);
}
......@@ -47,3 +47,12 @@ TEST_F(JsonPointerTest, InvalidPointers) {
json_pointer::parse_error::INVALID_ESCAPE_SEQUENCE,
json_pointer::try_parse("/~x").error());
}
TEST_F(JsonPointerTest, IsPrefixTo) {
EXPECT_TRUE(
json_pointer::parse("/a/b").is_prefix_of(json_pointer::parse("/a/b/c")));
EXPECT_FALSE(
json_pointer::parse("/a/b").is_prefix_of(json_pointer::parse("/a/d/e")));
EXPECT_FALSE(
json_pointer::parse("/a/b/c").is_prefix_of(json_pointer::parse("/a/b")));
}
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