Commit e7c48a6e authored by Andrew Krieger's avatar Andrew Krieger Committed by Facebook GitHub Bot

dynamic_view for safer traversal of dynamics

Summary:
Existing accessors on dynamic are cumbersome when operating on dynamics
that have optional keys or nullable values. Safely accessing these ends
up in very bloated or ugly client code, even when using getDefault().
Additionally, the move semantics of `dynamic` are usually inferred based
on the cv-ref qualification of the dynamic, and results in inconsistent
syntax depending on the situation.

`dynamic_view` attempts to resolve these issues by providing a new and
explicit API for traversing and accessing a `dynamic`. The main new
API is `descend`, which takes a varags list of keys and, functionally,
repeatedly applies `operator[]` and either returns a view of the resulting
value or an empty view if it doesn't exist or a type error occured during
descent. It only throws if another unexpected exception occurs.

`const_dynamic_view` contains copying accessors which will not throw
on a type mismatch, or if invoked on an empty view, but instead will
return a mandatory default value provided by the caller.

The other new API is only provided by the non-const `dynamic_view` class.
It is a set of value accessors which explicitly move the underlying value
out of the viewed dynamic. There is a specialization for string values
and a generic version for `dynamic`.

A helper method, `folly::make_dynamic_view` is provided which determines
the appropriate variant for a given dynamic ref.

`friend` access to `dynamic` allows optimized checks and accesses over the
regular `dynamic` APIs.

Reviewed By: yfeldblum

Differential Revision: D4917260

fbshipit-source-id: a07d26ed502aa22a5f19dd8521373a9e83fd6c02
parent 57e3562c
......@@ -1137,6 +1137,166 @@ inline std::ostream& operator<<(std::ostream& out, dynamic const& d) {
//////////////////////////////////////////////////////////////////////
inline const_dynamic_view::const_dynamic_view(dynamic const& d) noexcept
: d_(&d) {}
inline const_dynamic_view::const_dynamic_view(dynamic const* d) noexcept
: d_(d) {}
inline const_dynamic_view::operator bool() const noexcept {
return !empty();
}
inline bool const_dynamic_view::empty() const noexcept {
return d_ == nullptr;
}
inline void const_dynamic_view::reset() noexcept {
d_ = nullptr;
}
template <typename Key, typename... Keys>
inline const_dynamic_view const_dynamic_view::descend(
Key const& key, Keys const&... keys) const noexcept {
return descend_(key, keys...);
}
template <typename Key1, typename Key2, typename... Keys>
inline dynamic const* const_dynamic_view::descend_(
Key1 const& key1, Key2 const& key2, Keys const&... keys) const noexcept {
if (!d_) {
return nullptr;
}
return const_dynamic_view{descend_unchecked_(key1)}.descend_(key2, keys...);
}
template <typename Key>
inline dynamic const* const_dynamic_view::descend_(
Key const& key) const noexcept {
if (!d_) {
return nullptr;
}
return descend_unchecked_(key);
}
template <typename Key>
inline dynamic::IfIsNonStringDynamicConvertible<Key, dynamic const*>
const_dynamic_view::descend_unchecked_(Key const& key) const noexcept {
if (auto* parray = d_->get_nothrow<dynamic::Array>()) {
if /* constexpr */ (!std::is_integral<Key>::value) {
return nullptr;
}
if (key < 0 || key >= parray->size()) {
return nullptr;
}
return &(*parray)[size_t(key)];
} else if (auto* pobject = d_->get_nothrow<dynamic::ObjectImpl>()) {
auto it = pobject->find(key);
if (it == pobject->end()) {
return nullptr;
}
return &it->second;
}
return nullptr;
}
inline dynamic const* const_dynamic_view::descend_unchecked_(
folly::StringPiece key) const noexcept {
if (auto* pobject = d_->get_nothrow<dynamic::ObjectImpl>()) {
auto it = pobject->find(key);
if (it == pobject->end()) {
return nullptr;
}
return &it->second;
}
return nullptr;
}
inline dynamic const_dynamic_view::value_or(dynamic&& val) const {
if (d_) {
return *d_;
}
return std::move(val);
}
template <typename T, typename... Args>
inline T const_dynamic_view::get_copy(Args&&... args) const {
if (auto* v = (d_ ? d_->get_nothrow<T>() : nullptr)) {
return *v;
}
return T(std::forward<Args>(args)...);
}
inline std::string const_dynamic_view::string_or(char const* val) const {
return get_copy<std::string>(val);
}
inline std::string const_dynamic_view::string_or(std::string val) const {
return get_copy<std::string>(std::move(val));
}
// Specialized version for StringPiece, FixedString, and other types which are
// not convertible to std::string, but can construct one from .data and .size
// to std::string. Will not trigger a copy unless data and size require it.
template <typename Stringish, typename>
inline std::string const_dynamic_view::string_or(Stringish&& val) const {
return get_copy(val.data(), val.size());
}
inline double const_dynamic_view::double_or(double val) const noexcept {
return get_copy<double>(val);
}
inline int64_t const_dynamic_view::int_or(int64_t val) const noexcept {
return get_copy<int64_t>(val);
}
inline bool const_dynamic_view::bool_or(bool val) const noexcept {
return get_copy<bool>(val);
}
inline dynamic_view::dynamic_view(dynamic& d) noexcept
: const_dynamic_view(d) {}
template <typename Key, typename... Keys>
inline dynamic_view dynamic_view::descend(
Key const& key, Keys const&... keys) const noexcept {
if (auto* child = const_dynamic_view::descend_(key, keys...)) {
return *const_cast<dynamic*>(child);
}
return {};
}
inline dynamic dynamic_view::move_value_or(dynamic&& val) noexcept {
if (d_) {
return std::move(*const_cast<dynamic*>(d_));
}
return std::move(val);
}
template <typename T, typename... Args>
inline T dynamic_view::get_move(Args&&... args) {
if (auto* v = (d_ ? const_cast<dynamic*>(d_)->get_nothrow<T>() : nullptr)) {
return std::move(*v);
}
return T(std::forward<Args>(args)...);
}
inline std::string dynamic_view::move_string_or(char const* val) {
return get_move<std::string>(val);
}
inline std::string dynamic_view::move_string_or(std::string val) noexcept {
return get_move<std::string>(std::move(val));
}
template <typename Stringish, typename>
inline std::string dynamic_view::move_string_or(Stringish&& val) {
return get_move<std::string>(val.begin(), val.end());
}
//////////////////////////////////////////////////////////////////////
// Secialization of FormatValue so dynamic objects can be formatted
template <>
class FormatValue<dynamic> {
......
......@@ -72,7 +72,9 @@ namespace folly {
//////////////////////////////////////////////////////////////////////
struct const_dynamic_view;
struct dynamic;
struct dynamic_view;
struct TypeError;
//////////////////////////////////////////////////////////////////////
......@@ -730,6 +732,8 @@ struct dynamic : private boost::operators<dynamic> {
std::size_t hash() const;
private:
friend struct const_dynamic_view;
friend struct dynamic_view;
friend struct TypeError;
struct ObjectImpl;
template <class T>
......@@ -796,6 +800,171 @@ struct dynamic : private boost::operators<dynamic> {
//////////////////////////////////////////////////////////////////////
/**
* This is a helper class for traversing an instance of dynamic and accessing
* the values within without risking throwing an exception. The primary use case
* is to help write cleaner code when using dynamic instances without strict
* schemas - eg. where keys may be missing, or present but with null values,
* when expecting non-null values.
*
* Some examples:
*
* dynamic twelve = 12;
* dynamic str = "string";
* dynamic map = dynamic::object("str", str)("twelve", 12);
*
* dynamic_view view{map};
* assert(view.descend("str").string_or("bad") == "string");
* assert(view.descend("twelve").int_or(-1) == 12);
* assert(view.descend("zzz").string_or("aaa") == "aaa");
*
* dynamic wrapper = dynamic::object("child", map);
* dynamic_view wrapper_view{wrapper};
*
* assert(wrapper_view.descend("child", "str").string_or("bad") == "string");
* assert(wrapper_view.descend("wrong", 0, "huh").value_or(nullptr).isNull());
*/
struct const_dynamic_view {
// Empty view.
const_dynamic_view() noexcept = default;
// Basic view constructor. Creates a view of the referenced dynamic.
/* implicit */ const_dynamic_view(dynamic const& d) noexcept;
const_dynamic_view(const_dynamic_view const&) noexcept = default;
const_dynamic_view& operator=(const_dynamic_view const&) noexcept = default;
// Allow conversion from mutable to immutable view.
/* implicit */ const_dynamic_view(dynamic_view& view) noexcept;
/* implicit */ const_dynamic_view& operator=(dynamic_view& view) noexcept;
// Never view a temporary.
explicit const_dynamic_view(dynamic&&) = delete;
// Returns true if this view is backed by a valid dynamic, false otherwise.
explicit operator bool() const noexcept;
// Returns true if this view is not backed by a dynamic, false otherwise.
bool empty() const noexcept;
// Resets the view to a default constructed state not backed by any dynamic.
void reset() noexcept;
// Traverse a dynamic by repeatedly applying operator[].
// If all keys are valid, then the returned view will be backed by the
// accessed dynamic, otherwise it will be empty.
template <typename Key, typename... Keys>
const_dynamic_view descend(
Key const& key, Keys const&... keys) const noexcept;
// Untyped accessor. Returns a copy of the viewed dynamic, or the default
// value given if this view is empty, or a null dynamic otherwise.
dynamic value_or(dynamic&& val = nullptr) const;
// The following accessors provide a read-only exception-safe API for
// accessing the underlying viewed dynamic. Unlike the main dynamic APIs,
// these follow a stricter contract, which also requires a caller-provided
// default argument.
// - TypeError will not be thrown. primitive accessors further are marked
// noexcept.
// - No type conversions are performed. If the viewed dynamic does not match
// the requested type, the default argument is returned instead.
// - If the view is empty, the default argument is returned instead.
std::string string_or(char const* val) const;
std::string string_or(std::string val) const;
template <
typename Stringish,
typename = std::enable_if_t<
is_detected_v<dynamic_detail::detect_construct_string, Stringish>>>
std::string string_or(Stringish&& val) const;
double double_or(double val) const noexcept;
int64_t int_or(int64_t val) const noexcept;
bool bool_or(bool val) const noexcept;
protected:
/* implicit */ const_dynamic_view(dynamic const* d) noexcept;
template <typename Key1, typename Key2, typename... Keys>
dynamic const* descend_(
Key1 const& key1, Key2 const& key2, Keys const&... keys) const noexcept;
template <typename Key>
dynamic const* descend_(Key const& key) const noexcept;
template <typename Key>
dynamic::IfIsNonStringDynamicConvertible<Key, dynamic const*>
descend_unchecked_(Key const& key) const noexcept;
dynamic const* descend_unchecked_(StringPiece key) const noexcept;
dynamic const* d_ = nullptr;
// Internal helper method for accessing a value by a type.
template <typename T, typename... Args>
T get_copy(Args&&... args) const;
};
struct dynamic_view : public const_dynamic_view {
// Empty view.
dynamic_view() noexcept = default;
// dynamic_view can be used to view non-const dynamics only.
/* implicit */ dynamic_view(dynamic& d) noexcept;
dynamic_view(dynamic_view const&) noexcept = default;
dynamic_view& operator=(dynamic_view const&) noexcept = default;
// dynamic_view can not view const dynamics.
explicit dynamic_view(dynamic const&) = delete;
// dynamic_view can not be initialized from a const_dynamic_view
explicit dynamic_view(const_dynamic_view const&) = delete;
// Like const_dynamic_view, but returns a dynamic_view.
template <typename Key, typename... Keys>
dynamic_view descend(Key const& key, Keys const&... keys) const noexcept;
// dynamic_view provides APIs which can mutably access the backed dynamic.
// 'mutably access' in this case means extracting the viewed dynamic or
// value to omit unnecessary copies. It does not mean writing through to
// the backed dynamic - this is still just a view, not a mutator.
// Moves the viewed dynamic into the returned value via std::move. If the view
// is not backed by a dynamic, returns a provided default, or a null dynamic.
// Postconditions for the backed dynamic are the same as for any dynamic that
// is moved-from. this->empty() == false.
dynamic move_value_or(dynamic&& val = nullptr) noexcept;
// Specific optimization for strings which can allocate, unlike the other
// scalar types. If the viewed dynamic is a string, the string value is
// std::move'd to initialize a new instance which is returned.
std::string move_string_or(std::string val) noexcept;
std::string move_string_or(char const* val);
template <
typename Stringish,
typename = std::enable_if_t<
is_detected_v<dynamic_detail::detect_construct_string, Stringish>>>
std::string move_string_or(Stringish&& val);
private:
template <typename T, typename... Args>
T get_move(Args&&... args);
};
// A helper method which returns a contextually-correct dynamic_view for the
// given view. If passed a dynamic const&, returns a const_dynamic_view, and
// if passed a dynamic&, returns a dynamic_view.
inline auto make_dynamic_view(dynamic const& d) {
return const_dynamic_view{d};
}
inline auto make_dynamic_view(dynamic& d) {
return dynamic_view{d};
}
auto make_dynamic_view(dynamic&&) = delete;
//////////////////////////////////////////////////////////////////////
} // namespace folly
#include <folly/dynamic-inl.h>
......@@ -90,6 +90,227 @@ TEST(Dynamic, Getters) {
EXPECT_THROW(dBool.getDouble(), TypeError);
}
TEST(Dynamic, ViewTruthinessTest) {
// An default constructed view is falsey.
folly::const_dynamic_view view{};
EXPECT_EQ((bool)view, false);
dynamic d{nullptr};
// A view of a dynamic that is null is still truthy.
// Assigning a dynamic to a view sets it to that dynamic.
view = d;
EXPECT_EQ((bool)view, true);
// reset() empties a view.
view.reset();
EXPECT_EQ((bool)view, false);
}
TEST(Dynamic, ViewDescendTruthinessTest) {
folly::const_dynamic_view view{};
dynamic obj = dynamic::object("key", "value");
dynamic arr = dynamic::array("one", "two", "three");
// Descending should return truthy views.
view = obj;
EXPECT_EQ((bool)view, true);
view = view.descend("key");
EXPECT_EQ((bool)view, true);
view = arr;
EXPECT_EQ((bool)view, true);
view = view.descend(1);
EXPECT_EQ((bool)view, true);
}
TEST(Dynamic, ViewDescendFailedTest) {
folly::const_dynamic_view view{};
dynamic nully{};
dynamic obj = dynamic::object("key", "value");
dynamic arr = dynamic::array("one", "two", "three");
// Failed descents should return empty views.
view = nully;
EXPECT_EQ((bool)view, true);
view = view.descend("not an object");
EXPECT_EQ((bool)view, false);
view = obj;
EXPECT_EQ((bool)view, true);
view = view.descend("missing key");
EXPECT_EQ((bool)view, false);
view = arr;
EXPECT_EQ((bool)view, true);
view = view.descend("not an object");
EXPECT_EQ((bool)view, false);
view = arr;
EXPECT_EQ((bool)view, true);
view = view.descend(5); // out of range
EXPECT_EQ((bool)view, false);
}
TEST(Dynamic, ViewScalarsTest) {
dynamic i = 5;
dynamic str = "hello";
double pi = 3.14;
dynamic d = pi;
dynamic b = true;
folly::const_dynamic_view view{};
view = i;
EXPECT_EQ(view.int_or(0), 5);
view = str;
EXPECT_EQ(view.string_or("default"), "hello");
view = d;
EXPECT_EQ(view.double_or(0.7), d.asDouble());
view = b;
EXPECT_EQ(view.bool_or(false), true);
}
TEST(Dynamic, ViewScalarsWrongTest) {
dynamic i = 5;
dynamic str = "hello";
double pi = 3.14;
dynamic d = pi;
dynamic b = true;
folly::const_dynamic_view view{};
view = i;
EXPECT_EQ(view.string_or("default"), "default");
view = str;
EXPECT_EQ(view.double_or(pi), pi);
view = d;
EXPECT_EQ(view.bool_or(false), false);
view = b;
EXPECT_EQ(view.int_or(777), 777);
}
TEST(Dynamic, ViewDescendObjectOnceTest) {
double d = 3.14;
dynamic obj =
dynamic::object("string", "hello")("int", 5)("double", d)("bool", true);
folly::const_dynamic_view view{obj};
EXPECT_EQ(view.descend("string").string_or(""), "hello");
EXPECT_EQ(view.descend("int").int_or(0), 5);
EXPECT_EQ(view.descend("double").double_or(0.0), d);
EXPECT_EQ(view.descend("bool").bool_or(false), true);
}
TEST(Dynamic, ViewDescendObjectTwiceTest) {
double d = 3.14;
dynamic nested =
dynamic::object("string", "hello")("int", 5)("double", d)("bool", true);
dynamic wrapper = dynamic::object("wrapped", nested);
folly::const_dynamic_view view{wrapper};
EXPECT_EQ(view.descend("wrapped").descend("string").string_or(""), "hello");
EXPECT_EQ(view.descend("wrapped").descend("int").int_or(0), 5);
EXPECT_EQ(view.descend("wrapped").descend("double").double_or(0.0), d);
EXPECT_EQ(view.descend("wrapped").descend("bool").bool_or(false), true);
EXPECT_EQ(view.descend("wrapped", "string").string_or(""), "hello");
EXPECT_EQ(view.descend("wrapped", "int").int_or(0), 5);
EXPECT_EQ(view.descend("wrapped", "double").double_or(0.0), d);
EXPECT_EQ(view.descend("wrapped", "bool").bool_or(false), true);
}
TEST(Dynamic, ViewDescendObjectMissingKeyTest) {
double d = 3.14;
dynamic nested =
dynamic::object("string", "string")("int", 5)("double", d)("bool", true);
dynamic wrapper = dynamic::object("wrapped", nested);
folly::const_dynamic_view view{wrapper};
EXPECT_EQ(view.descend("wrapped").descend("string").string_or(""), "string");
EXPECT_EQ(view.descend("wrapped").descend("int").int_or(0), 5);
EXPECT_EQ(view.descend("wrapped").descend("double").double_or(0.0), d);
EXPECT_EQ(view.descend("wrapped").descend("bool").bool_or(false), true);
EXPECT_EQ(view.descend("wrapped", "string").string_or(""), "string");
EXPECT_EQ(view.descend("wrapped", "int").int_or(0), 5);
EXPECT_EQ(view.descend("wrapped", "double").double_or(0.0), d);
EXPECT_EQ(view.descend("wrapped", "bool").bool_or(false), true);
}
TEST(Dynamic, ViewDescendObjectAndArrayTest) {
dynamic leaf = dynamic::object("key", "value");
dynamic arr = dynamic::array(leaf);
dynamic root = dynamic::object("arr", arr);
folly::const_dynamic_view view{root};
EXPECT_EQ(view.descend("arr", 0, "key").string_or(""), "value");
}
std::string make_long_string() {
return std::string(100, 'a');
}
TEST(Dynamic, ViewMoveValuesTest) {
dynamic leaf = dynamic::object("key", make_long_string());
dynamic obj = dynamic::object("leaf", std::move(leaf));
folly::dynamic_view view{obj};
const dynamic value = view.descend("leaf").move_value_or(nullptr);
EXPECT_TRUE(value.isObject());
EXPECT_EQ(value.count("key"), 1);
EXPECT_TRUE(value["key"].isString());
EXPECT_EQ(value["key"].getString(), make_long_string());
// Original dynamic should have a moved-from "leaf" key.
EXPECT_EQ(obj.count("leaf"), 1);
EXPECT_TRUE(obj["leaf"].isObject());
EXPECT_EQ(obj["leaf"].count("key"), 0);
}
TEST(Dynamic, ViewMoveStringsTest) {
dynamic obj = dynamic::object("long_string", make_long_string());
folly::dynamic_view view{obj};
std::string value = view.descend("long_string").move_string_or("");
EXPECT_EQ(value, make_long_string());
// Original dynamic should still have a "long_string" entry with moved-from
// string value.
EXPECT_EQ(obj.count("long_string"), 1);
EXPECT_TRUE(obj["long_string"].isString());
EXPECT_NE(obj["long_string"].getString(), make_long_string());
}
TEST(Dynamic, ViewMakerTest) {
dynamic d{nullptr};
auto view = folly::make_dynamic_view(d);
EXPECT_TRUE((std::is_same<decltype(view), folly::dynamic_view>::value));
const dynamic cd{nullptr};
auto cv = folly::make_dynamic_view(cd);
EXPECT_TRUE((std::is_same<decltype(cv), folly::const_dynamic_view>::value));
// This should not compile, because you can't view temporaries.
// auto fail = folly::make_dynamic_view(folly::dynamic{nullptr});
}
TEST(Dynamic, FormattedIO) {
std::ostringstream out;
dynamic doubl = 123.33;
......
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