Added dynamic::convertTo<Type> method
authorNicholas Ormrod <njormrod@fb.com>
Tue, 14 Aug 2012 23:11:03 +0000 (16:11 -0700)
committerTudor Bosman <tudorb@fb.com>
Sun, 26 Aug 2012 18:13:09 +0000 (11:13 -0700)
Summary: convert a dynamic to a well-typed object

Test Plan: run test file

Reviewed By: delong.j@fb.com

FB internal diff: D517021

folly/DynamicConverter.h [new file with mode: 0644]
folly/docs/Dynamic.md
folly/docs/DynamicConverter.md [new file with mode: 0644]
folly/test/DynamicConverterTest.cpp [new file with mode: 0644]

diff --git a/folly/DynamicConverter.h b/folly/DynamicConverter.h
new file mode 100644 (file)
index 0000000..275d6d9
--- /dev/null
@@ -0,0 +1,277 @@
+/*
+ * Copyright 2012 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.
+ */
+
+// @author Nicholas Ormrod <njormrod@fb.com>
+
+#ifndef DYNAMIC_CONVERTER_H
+#define DYNAMIC_CONVERTER_H
+
+#include "folly/dynamic.h"
+namespace folly {
+  template <typename T> T convertTo(const dynamic&);
+}
+
+/**
+ * convertTo returns a well-typed representation of the input dynamic.
+ *
+ * Example:
+ *
+ *   dynamic d = { { 1, 2, 3 }, { 4, 5 } }; // a vector of vector of int
+ *   auto vvi = convertTo<fbvector<fbvector<int>>>(d);
+ *
+ * See docs/DynamicConverter.md for supported types and customization
+ */
+
+
+#include <type_traits>
+#include <boost/iterator/iterator_adaptor.hpp>
+#include <boost/mpl/has_xxx.hpp>
+#include "folly/Likely.h"
+
+namespace folly {
+
+///////////////////////////////////////////////////////////////////////////////
+// traits
+
+namespace dynamicconverter_detail {
+
+BOOST_MPL_HAS_XXX_TRAIT_DEF(value_type);
+BOOST_MPL_HAS_XXX_TRAIT_DEF(key_type);
+BOOST_MPL_HAS_XXX_TRAIT_DEF(mapped_type);
+BOOST_MPL_HAS_XXX_TRAIT_DEF(iterator);
+
+template <typename T> struct map_container_has_correct_types
+  : std::is_same<std::pair<typename std::add_const<typename T::key_type>::type,
+                           typename T::mapped_type>,
+                 typename T::value_type> {};
+
+template <typename T> struct class_is_container {
+  struct dummy {};
+  enum { value = has_value_type<T>::value &&
+                 has_iterator<T>::value &&
+                 std::is_constructible<T, dummy, dummy>::value };
+};
+
+template <typename T> struct container_is_map
+  : std::conditional<
+      has_key_type<T>::value && has_mapped_type<T>::value,
+      map_container_has_correct_types<T>,
+      std::false_type
+    >::type {};
+
+template <typename T> struct is_container
+  : std::conditional<
+      std::is_class<T>::value,
+      class_is_container<T>,
+      std::false_type
+    >::type {};
+
+template <typename T> struct is_map_container
+  : std::conditional<
+      is_container<T>::value,
+      container_is_map<T>,
+      std::false_type
+    >::type {};
+
+} // namespace dynamicconverter_detail
+
+///////////////////////////////////////////////////////////////////////////////
+// custom iterators
+
+/**
+ * We have iterators that dereference to dynamics, but need iterators
+ * that dereference to typename T.
+ *
+ * Implementation details:
+ *   1. We cache the value of the dereference operator. This is necessary
+ *      because boost::iterator_adaptor requires *it to return a
+ *      reference.
+ *   2. For const reasons, we cannot call operator= to refresh the
+ *      cache: we must call the destructor then placement new.
+ */
+
+namespace dynamicconverter_detail {
+
+template <typename F, typename S>
+inline void
+derefToCache(std::pair<F, S>* mem, const dynamic::const_item_iterator& it) {
+  new (mem) std::pair<F, S>(convertTo<F>(it->first), convertTo<S>(it->second));
+}
+
+template <typename T>
+inline void derefToCache(T* mem, const dynamic::const_iterator& it) {
+  new (mem) T(convertTo<T>(*it));
+}
+
+template <typename T, typename It>
+class Transformer : public boost::iterator_adaptor<
+                             Transformer<T, It>,
+                             It,
+                             typename T::value_type
+                           > {
+  friend class boost::iterator_core_access;
+
+  typedef typename T::value_type ttype;
+
+  mutable ttype cache_;
+  mutable bool valid_;
+
+  void increment() {
+    ++this->base_reference();
+    valid_ = false;
+  }
+
+  ttype& dereference() const {
+    if (LIKELY(!valid_)) {
+      cache_.~ttype();
+      derefToCache(&cache_, this->base_reference());
+      valid_ = true;
+    }
+    return cache_;
+  }
+
+public:
+  explicit Transformer(const It& it)
+    : Transformer::iterator_adaptor_(it), valid_(false) {}
+};
+
+// conversion factory
+template <typename T, typename It>
+static inline std::move_iterator<Transformer<T, It>>
+conversionIterator(const It& it) {
+  return std::make_move_iterator(Transformer<T, It>(it));
+}
+
+} // namespace dynamicconverter_detail
+
+///////////////////////////////////////////////////////////////////////////////
+// DynamicConverter specializations
+
+template <typename T, typename Enable = void> struct DynamicConverter;
+
+/**
+ * Each specialization of DynamicConverter has the function
+ *     'static T convert(const dynamic& d);'
+ */
+
+// boolean
+template <>
+struct DynamicConverter<bool> {
+  static bool convert(const dynamic& d) {
+    return d.asBool();
+  }
+};
+
+// integrals
+template <typename T>
+struct DynamicConverter<T,
+    typename std::enable_if<std::is_integral<T>::value &&
+                            !std::is_same<T, bool>::value>::type> {
+  static T convert(const dynamic& d) {
+    return static_cast<T>(d.asInt());
+  }
+};
+
+// floating point
+template <typename T>
+struct DynamicConverter<T,
+    typename std::enable_if<std::is_floating_point<T>::value>::type> {
+  static T convert(const dynamic& d) {
+    return static_cast<T>(d.asDouble());
+  }
+};
+
+// fbstring
+template <>
+struct DynamicConverter<folly::fbstring> {
+  static folly::fbstring convert(const dynamic& d) {
+    return d.asString();
+  }
+};
+
+// std::string
+template <>
+struct DynamicConverter<std::string> {
+  static std::string convert(const dynamic& d) {
+    return d.asString().toStdString();
+  }
+};
+
+// std::pair
+template <typename F, typename S>
+struct DynamicConverter<std::pair<F,S>> {
+  static std::pair<F, S> convert(const dynamic& d) {
+    if (d.isArray() && d.size() == 2) {
+      return std::make_pair(convertTo<F>(d[0]), convertTo<S>(d[1]));
+    } else if (d.isObject() && d.size() == 1) {
+      auto it = d.items().begin();
+      return std::make_pair(convertTo<F>(it->first), convertTo<S>(it->second));
+    } else {
+      throw TypeError("array (size 2) or object (size 1)", d.type());
+    }
+  }
+};
+
+// map containers
+template <typename C>
+struct DynamicConverter<C,
+    typename std::enable_if<
+      dynamicconverter_detail::is_map_container<C>::value>::type> {
+  static C convert(const dynamic& d) {
+    if (LIKELY(d.isObject())) {
+      return C(dynamicconverter_detail::conversionIterator<C>
+                 (d.items().begin()),
+               dynamicconverter_detail::conversionIterator<C>
+                 (d.items().end()));
+    } else if (d.isArray()) {
+      return C(dynamicconverter_detail::conversionIterator<C>(d.begin()),
+               dynamicconverter_detail::conversionIterator<C>(d.end()));
+    } else {
+      throw TypeError("object or array", d.type());
+    }
+  }
+};
+
+// non-map containers
+template <typename C>
+struct DynamicConverter<C,
+      typename std::enable_if<
+          dynamicconverter_detail::is_container<C>::value &&
+          !dynamicconverter_detail::is_map_container<C>::value
+        >::type
+    > {
+  static C convert(const dynamic& d) {
+    if (LIKELY(d.isArray())) {
+      return C(dynamicconverter_detail::conversionIterator<C>(d.begin()),
+               dynamicconverter_detail::conversionIterator<C>(d.end()));
+    } else {
+      throw TypeError("array", d.type());
+    }
+  }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+// convertTo implementation
+
+template <typename T>
+T convertTo(const dynamic& d) {
+  return DynamicConverter<T>::convert(d);
+}
+
+} // namespace folly
+
+#endif // DYNAMIC_CONVERTER_H
+
index 9a2beee3462e562483d727733c3960c128020e5f..c9a5c3652ba4fb6dd77855c163d2bd16715e7449 100644 (file)
@@ -73,6 +73,8 @@ Explicit type conversions can be requested for some of the basic types:
                                             // since it can't fit in a double
 ```
 
+For more complicated conversions, see [DynamicConverter](DynamicConverter.md).
+
 ### Iteration and Lookup
 ***
 
diff --git a/folly/docs/DynamicConverter.md b/folly/docs/DynamicConverter.md
new file mode 100644 (file)
index 0000000..d0ee7f7
--- /dev/null
@@ -0,0 +1,72 @@
+`folly/DynamicConverter.h`
+--------------------------
+
+When dynamic objects contain data of a known type, it is sometimes
+useful to have its well-typed representation. A broad set of
+type-conversions are contained in `DynamicConverter.h`, and
+facilitate the transformation of dynamic objects into their well-typed
+format.
+
+### Usage
+***
+
+Simply pass a dynamic into a templated convertTo:
+
+```
+    dynamic d = { { 1, 2, 3 }, { 4, 5 } }; // a vector of vector of int
+    auto vvi = convertTo<fbvector<fbvector<int>>>(d);
+```
+
+### Supported Types
+***
+
+convertTo naturally supports conversions to
+
+1. arithmetic types (such as int64_t, unsigned short, bool, and double)
+2. fbstring, std::string
+3. containers and map-containers
+
+NOTE:
+
+convertTo<Type> will assume that Type is a container if
+* it has a Type::value_type, and
+* it has a Type::iterator, and
+* it has a constructor that accepts two InputIterators
+
+Additionally, convertTo<Type> will assume that Type is a map if
+* it has a Type::key_type, and
+* it has a Type::mapped_type, and
+* value_type is a pair of const key_type and mapped_type
+
+If Type meets the container criteria, then it will be constructed
+by calling its InputIterator constructor.
+
+### Customization
+***
+
+If you want to use convertTo to convert dynamics into your own custom
+class, then all you have to do is provide a template specialization
+of DynamicConverter with the static method convert. Make sure you put it
+in namespace folly.
+
+Example:
+
+``` Cpp
+    struct Token {
+      int kind_;
+      fbstring lexeme_;
+      
+      explicit Token(int kind, const fbstring& lexeme)
+        : kind_(kind), lexeme_(lexeme) {}
+    };
+    namespace folly {
+    template <> struct DynamicConverter<Token> {
+      static Token convert(const dynamic& d) {
+        int k = convertTo<int>(d["KIND"]);
+        fbstring lex = convertTo<fbstring>(d["LEXEME"]);
+        return Token(k, lex);
+      }
+    };
+    }
+```
+
diff --git a/folly/test/DynamicConverterTest.cpp b/folly/test/DynamicConverterTest.cpp
new file mode 100644 (file)
index 0000000..5d414d4
--- /dev/null
@@ -0,0 +1,261 @@
+/*
+ * Copyright 2012 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.
+ */
+
+// @author Nicholas Ormrod <njormrod@fb.com>
+
+#include "folly/DynamicConverter.h"
+#include <gtest/gtest.h>
+#include <gflags/gflags.h>
+#include "folly/Benchmark.h"
+
+using namespace folly;
+using namespace folly::dynamicconverter_detail;
+
+TEST(DynamicConverter, template_metaprogramming) {
+  struct A {};
+
+  bool c1f = is_container<int>::value;
+  bool c2f = is_container<std::pair<int, int>>::value;
+  bool c3f = is_container<A>::value;
+
+  bool c1t = is_container<std::vector<int>>::value;
+  bool c2t = is_container<std::set<int>>::value;
+  bool c3t = is_container<std::map<int, int>>::value;
+
+  EXPECT_EQ(c1f, false);
+  EXPECT_EQ(c2f, false);
+  EXPECT_EQ(c3f, false);
+  EXPECT_EQ(c1t, true);
+  EXPECT_EQ(c2t, true);
+  EXPECT_EQ(c3t, true);
+
+  bool m1f = is_map_container<int>::value;
+  bool m2f = is_map_container<std::vector<int>>::value;
+  bool m3f = is_map_container<std::set<int>>::value;
+
+  bool m1t = is_map_container<std::map<int, int>>::value;
+  bool m2t = is_map_container<std::unordered_map<int, int>>::value;
+
+  EXPECT_EQ(m1f, false);
+  EXPECT_EQ(m2f, false);
+  EXPECT_EQ(m3f, false);
+  EXPECT_EQ(m1t, true);
+  EXPECT_EQ(m2t, true);
+}
+
+TEST(DynamicConverter, arithmetic_types) {
+  dynamic d1 = 12;
+  auto i1 = convertTo<int>(d1);
+  EXPECT_EQ(i1, 12);
+
+  dynamic d2 = 123456789012345;
+  auto i2 = convertTo<int64_t>(d2);
+  EXPECT_EQ(i2, 123456789012345);
+
+  dynamic d3 = 123456789012345;
+  auto i3 = convertTo<uint8_t>(d3);
+  EXPECT_EQ(i3, 121);
+
+  dynamic d4 = 3.141;
+  auto i4 = convertTo<float>(d4);
+  EXPECT_EQ((int)(i4*100), 314);
+
+  dynamic d5 = true;
+  auto i5 = convertTo<bool>(d5);
+  EXPECT_EQ(i5, true);
+
+  dynamic d6 = 15;
+  const auto i6 = convertTo<const int>(d6);
+  EXPECT_EQ(i6, 15);
+
+  dynamic d7 = "87";
+  auto i7 = convertTo<int>(d7);
+  EXPECT_EQ(i7, 87);
+
+  dynamic d8 = "false";
+  auto i8 = convertTo<bool>(d8);
+  EXPECT_EQ(i8, false);
+}
+
+TEST(DynamicConverter, simple_builtins) {
+  dynamic d1 = "Haskell";
+  auto i1 = convertTo<folly::fbstring>(d1);
+  EXPECT_EQ(i1, "Haskell");
+
+  dynamic d2 = 13;
+  auto i2 = convertTo<std::string>(d2);
+  EXPECT_EQ(i2, "13");
+
+  dynamic d3 = { 12, "Scala" };
+  auto i3 = convertTo<std::pair<int, std::string>>(d3);
+  EXPECT_EQ(i3.first, 12);
+  EXPECT_EQ(i3.second, "Scala");
+
+  dynamic d4 = dynamic::object("C", "C++");
+  auto i4 = convertTo<std::pair<std::string, folly::fbstring>>(d4);
+  EXPECT_EQ(i4.first, "C");
+  EXPECT_EQ(i4.second, "C++");
+}
+
+TEST(DynamicConverter, simple_fbvector) {
+  dynamic d1 = { 1, 2, 3 };
+  auto i1 = convertTo<folly::fbvector<int>>(d1);
+  decltype(i1) i1b = { 1, 2, 3 };
+  EXPECT_EQ(i1, i1b);
+}
+
+TEST(DynamicConverter, simple_container) {
+  dynamic d1 = { 1, 2, 3 };
+  auto i1 = convertTo<std::vector<int>>(d1);
+  decltype(i1) i1b = { 1, 2, 3 };
+  EXPECT_EQ(i1, i1b);
+
+  dynamic d2 = { 1, 3, 5, 2, 4 };
+  auto i2 = convertTo<std::set<int>>(d2);
+  decltype(i2) i2b = { 1, 2, 3, 5, 4 };
+  EXPECT_EQ(i2, i2b);
+}
+
+TEST(DynamicConverter, simple_map) {
+  dynamic d1 = dynamic::object(1, "one")(2, "two");
+  auto i1 = convertTo<std::map<int, std::string>>(d1);
+  decltype(i1) i1b = { { 1, "one" }, { 2, "two" } };
+  EXPECT_EQ(i1, i1b);
+
+  dynamic d2 = { { 3, "three" }, { 4, "four" } };
+  auto i2 = convertTo<std::unordered_map<int, std::string>>(d2);
+  decltype(i2) i2b = { { 3, "three" }, { 4, "four" } };
+  EXPECT_EQ(i2, i2b);
+}
+
+TEST(DynamicConverter, nested_containers) {
+  dynamic d1 = { { 1 }, { }, { 2, 3 } };
+  auto i1 = convertTo<folly::fbvector<std::vector<uint8_t>>>(d1);
+  decltype(i1) i1b = { { 1 }, { }, { 2, 3 } };
+  EXPECT_EQ(i1, i1b);
+
+  dynamic h2a = { "3", ".", "1", "4" };
+  dynamic h2b = { "2", ".", "7", "2" };
+  dynamic d2 = dynamic::object(3.14, h2a)(2.72, h2b);
+  auto i2 = convertTo<std::map<double, std::vector<folly::fbstring>>>(d2);
+  decltype(i2) i2b =
+    { { 3.14, { "3", ".", "1", "4" } },
+      { 2.72, { "2", ".", "7", "2" } } };
+  EXPECT_EQ(i2, i2b);
+}
+
+struct A {
+  int i;
+  bool operator==(const A & o) const { return i == o.i; }
+};
+namespace folly {
+template <> struct DynamicConverter<A> {
+  static A convert(const dynamic & d) {
+    return { convertTo<int>(d["i"]) };
+  }
+};
+}
+TEST(DynamicConverter, custom_class) {
+  dynamic d1 = dynamic::object("i", 17);
+  auto i1 = convertTo<A>(d1);
+  EXPECT_EQ(i1.i, 17);
+
+  dynamic d2 = { dynamic::object("i", 18), dynamic::object("i", 19) };
+  auto i2 = convertTo<std::vector<A>>(d2);
+  decltype(i2) i2b = { { 18 }, { 19 } };
+  EXPECT_EQ(i2, i2b);
+}
+
+TEST(DynamicConverter, crazy) {
+  // we are going to create a vector<unordered_map<bool, T>>
+  // we will construct some of the maps from dynamic objects,
+  //   some from a vector of KV pairs.
+  // T will be vector<set<string>>
+
+  std::set<std::string>
+    s1 = { "a", "e", "i", "o", "u" },
+    s2 = { "2", "3", "5", "7" },
+    s3 = { "Hello", "World" };
+
+  std::vector<std::set<std::string>>
+    v1 = {},
+    v2 = { s1, s2 },
+    v3 = { s3 };
+
+  std::unordered_map<bool, std::vector<std::set<std::string>>>
+    m1 = { { true, v1 }, { false, v2 } },
+    m2 = { { true, v3 } };
+
+  std::vector<std::unordered_map<bool, std::vector<std::set<std::string>>>>
+    f1 = { m1, m2 };
+
+
+  dynamic
+    ds1 = { "a", "e", "i", "o", "u" },
+    ds2 = { "2", "3", "5", "7" },
+    ds3 = { "Hello", "World" };
+
+  dynamic
+    dv1 = {},
+    dv2 = { ds1, ds2 },
+    dv3 = { ds3 };
+
+  dynamic
+    dm1 = dynamic::object(true, dv1)(false, dv2),
+    dm2 = { { true, dv3 } };
+
+  dynamic
+    df1 = { dm1, dm2 };
+
+
+  auto i = convertTo<std::vector<std::unordered_map<bool, std::vector<
+          std::set<std::string>>>>>(df1); // yes, that is 5 close-chevrons
+
+  EXPECT_EQ(f1, i);
+}
+
+struct Token {
+  int kind_;
+  fbstring lexeme_;
+
+  explicit Token(int kind, const fbstring& lexeme)
+    : kind_(kind), lexeme_(lexeme) {}
+};
+namespace folly {
+template <> struct DynamicConverter<Token> {
+  static Token convert(const dynamic& d) {
+    int k = convertTo<int>(d["KIND"]);
+    fbstring lex = convertTo<fbstring>(d["LEXEME"]);
+    return Token(k, lex);
+  }
+};
+}
+TEST(DynamicConverter, example) {
+  dynamic d1 = dynamic::object("KIND", 2)("LEXEME", "a token");
+  auto i1 = convertTo<Token>(d1);
+  EXPECT_EQ(i1.kind_, 2);
+  EXPECT_EQ(i1.lexeme_, "a token");
+}
+
+int main(int argc, char ** argv) {
+  testing::InitGoogleTest(&argc, argv);
+  google::ParseCommandLineFlags(&argc, &argv, true);
+  if (FLAGS_benchmark) {
+    folly::runBenchmarks();
+  }
+  return RUN_ALL_TESTS();
+}
+