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

Introduce try_get_ptr() method to query elements by JSON pointer

Summary: Previously `get_ptr(json_pointer const&)` would return nullptr or throw. The new try_get_ptr version provides richer facilities: points which token caused resolution error, if any, and also provides pointer to last-known properly resolved element. This allows, for example, implementing append logic with pointers like "/foo/bar/-" if "bar" is an array. The error would return `dynamic*` for the array, allowing caller to immediately perform push operation. Consequently, the get_ptr() is now implemented in terms of `try_get_ptr`.

Reviewed By: yfeldblum

Differential Revision: D10098092

fbshipit-source-id: 620996a66823be661d64d39661837cedf3c24493
parent 1dcceaf5
......@@ -783,6 +783,27 @@ inline dynamic* dynamic::get_ptr(StringPiece idx) & {
return const_cast<dynamic*>(const_cast<dynamic const*>(this)->get_ptr(idx));
}
// clang-format off
inline
dynamic::resolved_json_pointer<dynamic>
dynamic::try_get_ptr(json_pointer const& jsonPtr) & {
auto ret = const_cast<dynamic const*>(this)->try_get_ptr(jsonPtr);
if (ret.hasValue()) {
return json_pointer_resolved_value<dynamic>{
const_cast<dynamic*>(ret.value().parent),
const_cast<dynamic*>(ret.value().value),
ret.value().parent_key, ret.value().parent_index};
} else {
return makeUnexpected(
json_pointer_resolution_error<dynamic>{
ret.error().error_code,
ret.error().index,
const_cast<dynamic*>(ret.error().context)}
);
}
}
// clang-format on
inline dynamic* dynamic::get_ptr(json_pointer const& jsonPtr) & {
return const_cast<dynamic*>(
const_cast<dynamic const*>(this)->get_ptr(jsonPtr));
......
......@@ -16,6 +16,7 @@
#include <numeric>
#include <folly/container/Enumerate.h>
#include <folly/dynamic.h>
#include <folly/Format.h>
......@@ -368,46 +369,111 @@ dynamic dynamic::merge_diff(const dynamic& source, const dynamic& target) {
return diff;
}
const dynamic* dynamic::get_ptr(json_pointer const& jsonPtr) const& {
// clang-format off
dynamic::resolved_json_pointer<dynamic const>
// clang-format on
dynamic::try_get_ptr(json_pointer const& jsonPtr) const& {
using err_code = json_pointer_resolution_error_code;
using error = json_pointer_resolution_error<dynamic const>;
auto const& tokens = jsonPtr.tokens();
if (tokens.empty()) {
return this;
return json_pointer_resolved_value<dynamic const>{
nullptr, this, {nullptr, nullptr}, 0};
}
dynamic const* dyn = this;
for (auto const& token : tokens) {
if (!dyn) {
return nullptr;
dynamic const* curr = this;
dynamic const* prev = nullptr;
size_t curr_idx{0};
StringPiece curr_key{};
for (auto&& it : enumerate(tokens)) {
// hit bottom but pointer not exhausted yet
if (!curr) {
return makeUnexpected(
error{err_code::json_pointer_out_of_bounds, it.index, prev});
}
// special case of parsing "/": lookup key with empty name
if (token.empty()) {
if (dyn->isObject()) {
dyn = dyn->get_ptr("");
prev = curr;
// handle lookup in array
if (auto const* parray = curr->get_nothrow<dynamic::Array>()) {
if (it->size() > 1 && it->at(0) == '0') {
return makeUnexpected(
error{err_code::index_has_leading_zero, it.index, prev});
}
// if last element of pointer is '-', this is an append operation
if (it->size() == 1 && it->at(0) == '-') {
// was '-' the last token in pointer?
if (it.index == tokens.size() - 1) {
return makeUnexpected(
error{err_code::append_requested, it.index, prev});
}
// Cannot resolve past '-' in an array
curr = nullptr;
continue;
}
throw_exception<TypeError>("object", dyn->type());
}
if (auto* parray = dyn->get_nothrow<dynamic::Array>()) {
if (token.size() > 1 && token.at(0) == '0') {
throw std::invalid_argument(
"Leading zero not allowed when indexing arrays");
auto const idx = tryTo<size_t>(*it);
if (!idx.hasValue()) {
return makeUnexpected(
error{err_code::index_not_numeric, it.index, prev});
}
// special case, always return non-existent
if (token.size() == 1 && token.at(0) == '-') {
dyn = nullptr;
continue;
if (idx.value() < parray->size()) {
curr = &(*parray)[idx.value()];
curr_idx = idx.value();
} else {
return makeUnexpected(
error{err_code::index_out_of_bounds, it.index, prev});
}
auto const idx = folly::to<size_t>(token);
dyn = idx < parray->size() ? &(*parray)[idx] : nullptr;
continue;
}
if (auto* pobject = dyn->get_nothrow<dynamic::ObjectImpl>()) {
auto const it = pobject->find(token);
dyn = it != pobject->end() ? &it->second : nullptr;
// handle lookup in object
if (auto const* pobject = curr->get_nothrow<dynamic::ObjectImpl>()) {
auto const sub_it = pobject->find(*it);
if (sub_it == pobject->end()) {
return makeUnexpected(error{err_code::key_not_found, it.index, prev});
}
curr = &sub_it->second;
curr_key = *it;
continue;
}
throw_exception<TypeError>("object/array", dyn->type());
return makeUnexpected(
error{err_code::element_not_object_or_array, it.index, prev});
}
return json_pointer_resolved_value<dynamic const>{
prev, curr, curr_key, curr_idx};
}
const dynamic* dynamic::get_ptr(json_pointer const& jsonPtr) const& {
using err_code = json_pointer_resolution_error_code;
auto ret = try_get_ptr(jsonPtr);
if (ret.hasValue()) {
return ret.value().value;
}
auto const ctx = ret.error().context;
auto const objType = ctx ? ctx->type() : Type::NULLT;
switch (ret.error().error_code) {
case err_code::key_not_found:
return nullptr;
case err_code::index_out_of_bounds:
return nullptr;
case err_code::append_requested:
return nullptr;
case err_code::index_not_numeric:
throw std::invalid_argument("array index is not numeric");
case err_code::index_has_leading_zero:
throw std::invalid_argument(
"leading zero not allowed when indexing arrays");
case err_code::element_not_object_or_array:
throw_exception<TypeError>("object/array", objType);
case err_code::json_pointer_out_of_bounds:
return nullptr;
default:
return nullptr;
}
return dyn;
assume_unreachable();
}
//////////////////////////////////////////////////////////////////////
......
......@@ -62,6 +62,7 @@
#include <boost/operators.hpp>
#include <folly/Expected.h>
#include <folly/Range.h>
#include <folly/Traits.h>
#include <folly/container/F14Map.h>
......@@ -433,9 +434,70 @@ struct dynamic : private boost::operators<dynamic> {
dynamic&& at(StringPiece) &&;
/*
* Locate element using JSON pointer, per RFC 6901. Returns nullptr if
* element could not be located. Throws if pointer does not match the
* shape of the document, e.g. uses string to index in array.
* Locate element using JSON pointer, per RFC 6901
*/
enum class json_pointer_resolution_error_code : uint8_t {
other = 0,
// key not found in object
key_not_found,
// array index out of bounds
index_out_of_bounds,
// special index "-" requesting append
append_requested,
// indexes in arrays must be numeric
index_not_numeric,
// indexes in arrays should not have leading zero
index_has_leading_zero,
// element not subscribable, i.e. neither object or array
element_not_object_or_array,
// hit document boundary, but pointer not exhausted yet
json_pointer_out_of_bounds,
};
template <typename Dynamic>
struct json_pointer_resolution_error {
// error code encountered while resolving JSON pointer
json_pointer_resolution_error_code error_code{};
// index of the JSON pointer's token that caused the error
size_t index{0};
// Last correctly resolved element in object. You can use it,
// for example, to add last element to the array
Dynamic* context{nullptr};
};
template <typename Dynamic>
struct json_pointer_resolved_value {
// parent element of the value in dynamic, if exist
Dynamic* parent{nullptr};
// pointer to the value itself
Dynamic* value{nullptr};
// if parent isObject, this is the key in object to get value
StringPiece parent_key;
// if parent isArray, this is the index in array to get value
size_t parent_index{0};
};
// clang-format off
template <typename Dynamic>
using resolved_json_pointer = Expected<
json_pointer_resolved_value<Dynamic>,
json_pointer_resolution_error<Dynamic>>;
resolved_json_pointer<dynamic const>
try_get_ptr(json_pointer const&) const&;
resolved_json_pointer<dynamic>
try_get_ptr(json_pointer const&) &;
resolved_json_pointer<dynamic const>
try_get_ptr(json_pointer const&) const&& = delete;
resolved_json_pointer<dynamic const>
try_get_ptr(json_pointer const&) && = delete;
// clang-format on
/*
* The following versions return nullptr if element could not be located.
* Throws if pointer does not match the shape of the document, e.g. uses
* string to index in array.
*/
const dynamic* get_ptr(json_pointer const&) const&;
dynamic* get_ptr(json_pointer const&) &;
......
......@@ -1051,6 +1051,8 @@ TEST(Dynamic, MergeDiffNestedObjects) {
using folly::json_pointer;
TEST(Dynamic, JSONPointer) {
using err_code = folly::dynamic::json_pointer_resolution_error_code;
dynamic target = dynamic::object;
dynamic ary = dynamic::array("bar", "baz", dynamic::array("bletch", "xyzzy"));
target["foo"] = ary;
......@@ -1093,22 +1095,114 @@ TEST(Dynamic, JSONPointer) {
// allow '-' to index in objects
EXPECT_EQ("y", target.get_ptr(json_pointer::parse("/-/x"))->getString());
// invalid JSON pointers formatting when accessing array
EXPECT_THROW(
target.get_ptr(json_pointer::parse("/foo/01")), std::invalid_argument);
// validate parent pointer functionality
{
auto const resolved_value =
target.try_get_ptr(json_pointer::parse("")).value();
EXPECT_EQ(nullptr, resolved_value.parent);
}
{
auto parent_json_ptr = json_pointer::parse("/xyz");
auto json_ptr = json_pointer::parse("/xyz/def");
auto const parent = target.get_ptr(parent_json_ptr);
auto const resolved_value = target.try_get_ptr(json_ptr);
EXPECT_EQ(parent, resolved_value.value().parent);
EXPECT_TRUE(parent->isObject());
EXPECT_EQ("def", resolved_value.value().parent_key);
}
{
auto parent_json_ptr = json_pointer::parse("/foo");
auto json_ptr = json_pointer::parse("/foo/1");
auto const parent = target.get_ptr(parent_json_ptr);
auto const resolved_value = target.try_get_ptr(json_ptr);
EXPECT_EQ(parent, resolved_value.value().parent);
EXPECT_TRUE(parent->isArray());
EXPECT_EQ(1, resolved_value.value().parent_index);
}
//
// invalid pointer resolution cases
//
// invalid index formatting when accessing array
{
auto err = target.try_get_ptr(json_pointer::parse("/foo/01")).error();
EXPECT_EQ(err_code::index_has_leading_zero, err.error_code);
EXPECT_EQ(1, err.index);
EXPECT_EQ(target.get_ptr(json_pointer::parse("/foo")), err.context);
EXPECT_THROW(
target.get_ptr(json_pointer::parse("/foo/01")), std::invalid_argument);
}
// non-existent keys/indexes
EXPECT_EQ(nullptr, ary.get_ptr(json_pointer::parse("/3")));
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/unknown_key")));
{
auto err = ary.try_get_ptr(json_pointer::parse("/3")).error();
EXPECT_EQ(err_code::index_out_of_bounds, err.error_code);
EXPECT_EQ(0, err.index);
EXPECT_EQ(ary.get_ptr(json_pointer::parse("")), err.context);
EXPECT_EQ(nullptr, ary.get_ptr(json_pointer::parse("/3")));
}
{
auto err = target.try_get_ptr(json_pointer::parse("/unknown_key")).error();
EXPECT_EQ(err_code::key_not_found, err.error_code);
EXPECT_EQ(0, err.index);
EXPECT_EQ(target.get_ptr(json_pointer::parse("")), err.context);
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/unknown_key")));
}
// fail to resolve index inside string
{
auto err = target.try_get_ptr(json_pointer::parse("/foo/0/0")).error();
EXPECT_EQ(err_code::element_not_object_or_array, err.error_code);
EXPECT_EQ(2, err.index);
EXPECT_EQ(target.get_ptr(json_pointer::parse("/foo/0")), err.context);
EXPECT_THROW(
target.get_ptr(json_pointer::parse("/foo/0/0")), folly::TypeError);
}
// intermediate key not found
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/foox/test")));
// Intermediate key is '-'
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/foo/-/key")));
{
auto err = target.try_get_ptr(json_pointer::parse("/foox/test")).error();
EXPECT_EQ(err_code::key_not_found, err.error_code);
EXPECT_EQ(0, err.index);
EXPECT_EQ(target.get_ptr(json_pointer::parse("")), err.context);
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/foox/test")));
}
// invalid path in object (key in array)
EXPECT_THROW(
target.get_ptr(json_pointer::parse("/foo/1/bar")), folly::TypeError);
// Intermediate key is '-' in _array_
{
auto err = target.try_get_ptr(json_pointer::parse("/foo/-/key")).error();
EXPECT_EQ(err_code::json_pointer_out_of_bounds, err.error_code);
EXPECT_EQ(2, err.index);
EXPECT_EQ(target.get_ptr(json_pointer::parse("/foo")), err.context);
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/foo/-/key")));
}
// invalid path in object (non-numeric index in array)
{
auto err = target.try_get_ptr(json_pointer::parse("/foo/2/bar")).error();
EXPECT_EQ(err_code::index_not_numeric, err.error_code);
EXPECT_EQ(2, err.index);
EXPECT_EQ(target.get_ptr(json_pointer::parse("/foo/2")), err.context);
EXPECT_THROW(
target.get_ptr(json_pointer::parse("/foo/2/bar")),
std::invalid_argument);
}
// Allow "-" index in the array
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/foo/-")));
{
auto err = target.try_get_ptr(json_pointer::parse("/foo/-")).error();
EXPECT_EQ(err_code::append_requested, err.error_code);
EXPECT_EQ(1, err.index);
EXPECT_EQ(target.get_ptr(json_pointer::parse("/foo")), err.context);
EXPECT_EQ(nullptr, target.get_ptr(json_pointer::parse("/foo/-")));
}
}
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