changeset 635:002c13ee0021

json_util: create parser class
author David Demelier <markand@malikania.fr>
date Mon, 26 Mar 2018 13:26:31 +0200
parents 005794d9cf77
children f36b978962fb
files cpp/json_util/json_util.hpp cpp/json_util/test/main.cpp
diffstat 2 files changed, 272 insertions(+), 200 deletions(-) [+]
line wrap: on
line diff
--- a/cpp/json_util/json_util.hpp	Fri Mar 23 12:56:47 2018 +0100
+++ b/cpp/json_util/json_util.hpp	Mon Mar 26 13:26:31 2018 +0200
@@ -25,7 +25,9 @@
  */
 
 #include <cstdint>
+#include <limits>
 #include <string>
+#include <type_traits>
 
 #include <boost/optional.hpp>
 
@@ -37,214 +39,282 @@
 namespace json_util {
 
 /**
- * Get a JSON value from the given object or array.
- *
- * \param json the JSON object/array
- * \param key the pointer to the object
- * \return the value or boost::none if not found
+ * \cond JSON_UTIL_HIDDEN_SYMBOLS
  */
-inline boost::optional<nlohmann::json> get(const nlohmann::json& json,
-                                           const nlohmann::json::json_pointer& key) noexcept
-{
-    // Unfortunately, there is no find using pointer yet.
-    try {
-        return json.at(key);
-    } catch (...) {
-        return boost::none;
+
+namespace detail {
+
+template <typename Int>
+class parser_type_traits_uint : public std::true_type {
+public:
+    static boost::optional<Int> get(const nlohmann::json& value) noexcept
+    {
+        if (!value.is_number_unsigned())
+            return boost::none;
+
+        const auto ret = value.get<std::uint64_t>();
+
+        if (ret > std::numeric_limits<Int>::max())
+            return boost::none;
+
+        return static_cast<Int>(ret);
     }
-}
+};
+
+template <typename Int>
+class parser_type_traits_int : public std::true_type {
+public:
+    static boost::optional<Int> get(const nlohmann::json& value) noexcept
+    {
+        if (!value.is_number_integer())
+            return boost::none;
+
+        const auto ret = value.get<std::int64_t>();
+
+        if (ret < std::numeric_limits<Int>::min() || ret > std::numeric_limits<Int>::max())
+            return boost::none;
+
+        return static_cast<Int>(ret);
+    }
+};
+
+} // !detail
 
 /**
- * Convenient overload with simple key.
- *
- * \param json the JSON object/array
- * \param key the pointer to the object
- * \return the value or boost::none if not found
+ * \endcond
  */
-inline boost::optional<nlohmann::json> get(const nlohmann::json& json,
-                                           const std::string& key) noexcept
-{
-    const auto it = json.find(key);
-
-    if (it == json.end())
-        return boost::none;
-
-    return *it;
-}
 
 /**
- * Get a bool or null if not found or invalid.
+ * \brief Describe how to convert a JSON value.
+ *
+ * This class must be specialized for every type you want to convert from JSON
+ * to its native type.
+ *
+ * You only need to implement the get function with the following signature:
  *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \return the value or boost::none if not found or invalid
+ * ```cpp
+ * static boost::optional<T> get(const nlohmann::json& value);
+ * ```
+ *
+ * The implementation should not throw an exception but return a null optional
+ * instead.
+ *
+ * This class is already specialized for the given types:
+ *
+ * - bool
+ * - double
+ * - std::uint(8, 16, 32, 64)
+ * - std::string
  */
-template <typename Key>
-inline boost::optional<bool> get_bool(const nlohmann::json& json, const Key& key) noexcept
-{
-    const auto v = get(json, key);
-
-    if (!v || !v->is_boolean())
-        return boost::none;
-
-    return v->template get<bool>();
-}
+template <typename T>
+class parser_type_traits : public std::false_type {
+};
 
 /**
- * Get a 64 bit signed integer or null if not found or invalid.
- *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \return the value or boost::none if not found or invalid
+ * \brief Specialization for `bool`.
  */
-template <typename Key>
-inline boost::optional<std::int64_t> get_int(const nlohmann::json& json, const Key& key) noexcept
-{
-    const auto v = get(json, key);
+template <>
+class parser_type_traits<bool> : public std::true_type {
+public:
+    /**
+     * Convert the JSON value to bool.
+     *
+     * \return the bool or none if not a boolean type
+     */
+    static boost::optional<bool> get(const nlohmann::json& value) noexcept
+    {
+        if (!value.is_boolean())
+            return boost::none;
 
-    if (!v || !v->is_number_integer())
-        return boost::none;
-
-    return v->template get<std::int64_t>();
-}
+        return value.get<bool>();
+    }
+};
 
 /**
- * Get a 64 bit unsigned integer or null if not found or invalid.
- *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \return the value or boost::none if not found or invalid
+ * \brief Specialization for `double`.
  */
-template <typename Key>
-inline boost::optional<std::uint64_t> get_uint(const nlohmann::json& json, const Key& key) noexcept
-{
-    const auto v = get(json, key);
+template <>
+class parser_type_traits<double> : public std::true_type {
+public:
+    /**
+     * Convert the JSON value to bool.
+     *
+     * \return the double or none if not a double type
+     */
+    static boost::optional<double> get(const nlohmann::json& value) noexcept
+    {
+        if (!value.is_number_float())
+            return boost::none;
+
+        return value.get<double>();
+    }
+};
 
-    if (!v || !v->is_number_unsigned())
-        return boost::none;
+/**
+ * \brief Specialization for `std::string`.
+ */
+template <>
+class parser_type_traits<std::string> : public std::true_type {
+public:
+    /**
+     * Convert the JSON value to bool.
+     *
+     * \return the string or none if not a string type
+     */
+    static boost::optional<std::string> get(const nlohmann::json& value)
+    {
+        if (!value.is_string())
+            return boost::none;
 
-    return v->template get<std::uint64_t>();
-}
+        return value.get<std::string>();
+    }
+};
 
 /**
- * Get a string or null if not found or invalid.
- *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \return the value or boost::none if not found or invalid
+ * \brief Specialization for `std::int8_t`.
+ */
+template <>
+class parser_type_traits<std::int8_t> : public detail::parser_type_traits_int<std::int8_t> {
+};
+
+/**
+ * \brief Specialization for `std::int16_t`.
  */
-template <typename Key>
-inline boost::optional<std::string> get_string(const nlohmann::json& json, const Key& key) noexcept
-{
-    const auto v = get(json, key);
+template <>
+class parser_type_traits<std::int16_t> : public detail::parser_type_traits_int<std::int16_t> {
+};
 
-    if (!v || !v->is_string())
-        return boost::none;
-
-    return v->template get<std::string>();
-}
+/**
+ * \brief Specialization for `std::int32_t`.
+ */
+template <>
+class parser_type_traits<std::int32_t> : public detail::parser_type_traits_int<std::int32_t> {
+};
 
 /**
- * Get an optional bool.
- *
- * If the property is not found, return default value. If the property is not
- * a bool, return boost::none, otherwise return the value.
- *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \param def the default value
- * \return the value, boost::none or def
+ * \brief Specialization for `std::int64_t`.
  */
-template <typename Key>
-inline boost::optional<bool> optional_bool(const nlohmann::json& json, const Key& key, bool def = false) noexcept
-{
-    const auto v = get(json, key);
+template <>
+class parser_type_traits<std::int64_t> : public std::true_type {
+public:
+    /**
+     * Convert the JSON value to std::int64_t.
+     *
+     * \return the int or none if not a int type
+     */
+    static boost::optional<std::int64_t> get(const nlohmann::json& value) noexcept
+    {
+        if (!value.is_number_integer())
+            return boost::none;
+
+        return value.get<std::int64_t>();
+    }
+};
 
-    if (!v)
-        return def;
-    if (!v->is_boolean())
-        return boost::none;
+/**
+ * \brief Specialization for `std::int8_t`.
+ */
+template <>
+class parser_type_traits<std::uint8_t> : public detail::parser_type_traits_uint<std::uint8_t> {
+};
 
-    return v->template get<bool>();
-}
+/**
+ * \brief Specialization for `std::int16_t`.
+ */
+template <>
+class parser_type_traits<std::uint16_t> : public detail::parser_type_traits_uint<std::uint16_t> {
+};
+
+/**
+ * \brief Specialization for `std::int32_t`.
+ */
+template <>
+class parser_type_traits<std::uint32_t> : public detail::parser_type_traits_uint<std::uint32_t> {
+};
 
 /**
- * Get an optional integer.
- *
- * If the property is not found, return default value. If the property is not
- * an integer, return boost::none, otherwise return the value.
- *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \param def the default value
- * \return the value, boost::none or def
+ * \brief Specialization for `std::int64_t`.
  */
-template <typename Key>
-inline boost::optional<std::int64_t> optional_int(const nlohmann::json& json,
-                                                  const Key& key,
-                                                  std::int64_t def = 0) noexcept
-{
-    const auto v = get(json, key);
+template <>
+class parser_type_traits<std::uint64_t> : public std::true_type {
+public:
+    /**
+     * Convert the JSON value to std::uint64_t.
+     *
+     * \return the int or none if not a int type
+     */
+    static boost::optional<std::uint64_t> get(const nlohmann::json& value) noexcept
+    {
+        if (!value.is_number_unsigned())
+            return boost::none;
 
-    if (!v)
-        return def;
-    if (!v->is_number_integer())
-        return boost::none;
-
-    return v->template get<std::int64_t>();
-}
+        return value.get<std::uint64_t>();
+    }
+};
 
 /**
- * Get an optional unsigned integer.
- *
- * If the property is not found, return default value. If the property is not
- * an unsigned integer, return boost::none, otherwise return the value.
+ * \brief Convenient JSON object parser
  *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \param def the default value
- * \return the value, boost::none or def
+ * This class helps destructuring insecure JSON input by returning optional
+ * values if they are not present or invalid.
  */
-template <typename Key>
-inline boost::optional<std::uint64_t> optional_uint(const nlohmann::json& json,
-                                                    const Key& key,
-                                                    std::uint64_t def = 0) noexcept
-{
-    const auto v = get(json, key);
+class parser {
+private:
+    nlohmann::json& json_;
 
-    if (!v)
-        return def;
-    if (!v->is_number_unsigned())
-        return boost::none;
+public:
+    /**
+     * Construct the parser.
+     *
+     * \param document the document
+     */
+    inline parser(nlohmann::json& document) noexcept
+        : json_(document)
+    {
+    }
 
-    return v->template get<std::uint64_t>();
-}
+    /**
+     * Get a value from the document object.
+     *
+     * \param key the property key
+     * \return the value or boost::none if not found
+     */
+    template <typename Type>
+    inline boost::optional<Type> get(const std::string& key) const noexcept
+    {
+        static_assert(parser_type_traits<Type>::value, "type not supported");
 
-/**
- * Get an optional string.
- *
- * If the property is not found, return default value. If the property is not
- * a string, return boost::none, otherwise return the value.
- *
- * \param json the JSON object/array
- * \param key the pointer or property key
- * \param def the default value
- * \return the value, boost::none or def
- */
-template <typename Key>
-inline boost::optional<std::string> optional_string(const nlohmann::json& json,
-                                                    const Key& key,
-                                                    const std::string& def = "") noexcept
-{
-    const auto v = get(json, key);
+        const auto it = json_.find(key);
+
+        if (it == json_.end())
+            return boost::none;
+
+        return parser_type_traits<Type>::get(*it);
+    }
 
-    if (!v)
-        return def;
-    if (!v->is_string())
-        return boost::none;
+    /**
+     * Get an optional value from the document object.
+     *
+     * If the value is undefined, the default value is returned. Otherwise, if
+     * the value is not in the given type, boost::none is returned.
+     *
+     * \param key the property key
+     * \param def the default value if property is undefined
+     * \return the value, boost::none or def
+     */
+    template <typename Type>
+    inline boost::optional<Type> optional(const std::string& key, Type&& def) const noexcept
+    {
+        static_assert(parser_type_traits<Type>::value, "type not supported");
 
-    return v->template get<std::string>();
-}
+        const auto it = json_.find(key);
+
+        if (it == json_.end())
+            return def;
+
+        return parser_type_traits<Type>::get(*it);
+    }
+};
 
 /**
  * Print the value as human readable.
--- a/cpp/json_util/test/main.cpp	Fri Mar 23 12:56:47 2018 +0100
+++ b/cpp/json_util/test/main.cpp	Mon Mar 26 13:26:31 2018 +0200
@@ -32,6 +32,8 @@
         { "uint",       1234U   },
         { "string",     "str"   }
     };
+
+    parser parser_{json_};
 };
 
 BOOST_FIXTURE_TEST_SUITE(test_fixture_suite, test_fixture)
@@ -40,46 +42,46 @@
 
 BOOST_AUTO_TEST_CASE(boolean)
 {
-    const auto v = get_bool(json_, "/boolean"_json_pointer);
+    const auto v = parser_.get<bool>("boolean");
 
     BOOST_TEST(v);
     BOOST_TEST(*v);
-    BOOST_TEST(!get_bool(json_, "/int"_json_pointer));
-    BOOST_TEST(!get_bool(json_, "/int"_json_pointer));
-    BOOST_TEST(!get_bool(json_, "/uint"_json_pointer));
-    BOOST_TEST(!get_bool(json_, "/string"_json_pointer));
+    BOOST_TEST(!parser_.get<bool>("int"));
+    BOOST_TEST(!parser_.get<bool>("int"));
+    BOOST_TEST(!parser_.get<bool>("uint"));
+    BOOST_TEST(!parser_.get<bool>("string"));
 }
 
 BOOST_AUTO_TEST_CASE(integer)
 {
-    const auto v = get_int(json_, "/int"_json_pointer);
+    const auto v = parser_.get<int>("int");
 
     BOOST_TEST(v);
     BOOST_TEST(*v == -1234);
-    BOOST_TEST(!get_int(json_, "/bool"_json_pointer));
-    BOOST_TEST(!get_int(json_, "/string"_json_pointer));
+    BOOST_TEST(!parser_.get<int>("bool"));
+    BOOST_TEST(!parser_.get<int>("string"));
 }
 
 BOOST_AUTO_TEST_CASE(unsigned_integer)
 {
-    const auto v = get_uint(json_, "/uint"_json_pointer);
+    const auto v = parser_.get<unsigned>("uint");
 
     BOOST_TEST(v);
     BOOST_TEST(*v == 1234U);
-    BOOST_TEST(!get_uint(json_, "/bool"_json_pointer));
-    BOOST_TEST(!get_uint(json_, "/int"_json_pointer));
-    BOOST_TEST(!get_uint(json_, "/string"_json_pointer));
+    BOOST_TEST(!parser_.get<unsigned>("bool"));
+    BOOST_TEST(!parser_.get<unsigned>("int"));
+    BOOST_TEST(!parser_.get<unsigned>("string"));
 }
 
 BOOST_AUTO_TEST_CASE(string)
 {
-    const auto v = get_string(json_, "/string"_json_pointer);
+    const auto v = parser_.get<std::string>("string");
 
     BOOST_TEST(v);
     BOOST_TEST(*v == "str");
-    BOOST_TEST(!get_string(json_, "/bool"_json_pointer));
-    BOOST_TEST(!get_string(json_, "/int"_json_pointer));
-    BOOST_TEST(!get_string(json_, "/uint"_json_pointer));
+    BOOST_TEST(!parser_.get<std::string>("bool"));
+    BOOST_TEST(!parser_.get<std::string>("int"));
+    BOOST_TEST(!parser_.get<std::string>("uint"));
 }
 
 BOOST_AUTO_TEST_SUITE_END()
@@ -88,23 +90,23 @@
 
 BOOST_AUTO_TEST_CASE(boolean)
 {
-    BOOST_TEST(*optional_bool(json_, "/undefined"_json_pointer, true));
-    BOOST_TEST(!*optional_bool(json_, "/undefined"_json_pointer, false));
+    BOOST_TEST(*parser_.optional<bool>("undefined", true));
+    BOOST_TEST(!*parser_.optional<bool>("undefined", false));
 }
 
 BOOST_AUTO_TEST_CASE(integer)
 {
-    BOOST_TEST(*optional_int(json_, "/undefined"_json_pointer, 1234) == 1234);
+    BOOST_TEST(*parser_.optional<int>("undefined", 1234) == 1234);
 }
 
 BOOST_AUTO_TEST_CASE(unsigned_integer)
 {
-    BOOST_TEST(*optional_uint(json_, "/undefined"_json_pointer, 1234U) == 1234U);
+    BOOST_TEST(*parser_.optional<unsigned>("undefined", 1234U) == 1234U);
 }
 
 BOOST_AUTO_TEST_CASE(string)
 {
-    BOOST_TEST(*optional_string(json_, "/undefined"_json_pointer, "foo") == "foo");
+    BOOST_TEST(*parser_.optional<std::string>("undefined", "foo") == "foo");
 }
 
 BOOST_AUTO_TEST_SUITE_END()
@@ -113,29 +115,29 @@
 
 BOOST_AUTO_TEST_CASE(boolean)
 {
-    BOOST_TEST(!optional_bool(json_, "/int"_json_pointer, true));
-    BOOST_TEST(!optional_bool(json_, "/uint"_json_pointer, true));
-    BOOST_TEST(!optional_bool(json_, "/string"_json_pointer, true));
+    BOOST_TEST(!parser_.optional<bool>("int", true));
+    BOOST_TEST(!parser_.optional<bool>("uint", true));
+    BOOST_TEST(!parser_.optional<bool>("string", true));
 }
 
 BOOST_AUTO_TEST_CASE(integer)
 {
-    BOOST_TEST(!optional_int(json_, "/boolean"_json_pointer, 1234));
-    BOOST_TEST(!optional_int(json_, "/string"_json_pointer, 1234));
+    BOOST_TEST(!parser_.optional<int>("boolean", 1234));
+    BOOST_TEST(!parser_.optional<int>("string", 1234));
 }
 
 BOOST_AUTO_TEST_CASE(unsigned_integer)
 {
-    BOOST_TEST(!optional_uint(json_, "/boolean"_json_pointer, 1234));
-    BOOST_TEST(!optional_uint(json_, "/int"_json_pointer, 1234));
-    BOOST_TEST(!optional_uint(json_, "/string"_json_pointer, 1234));
+    BOOST_TEST(!parser_.optional<unsigned>("boolean", 1234));
+    BOOST_TEST(!parser_.optional<unsigned>("int", 1234));
+    BOOST_TEST(!parser_.optional<unsigned>("string", 1234));
 }
 
 BOOST_AUTO_TEST_CASE(string)
 {
-    BOOST_TEST(!optional_string(json_, "/boolean"_json_pointer, "foo"));
-    BOOST_TEST(!optional_string(json_, "/int"_json_pointer, "foo"));
-    BOOST_TEST(!optional_string(json_, "/uint"_json_pointer, "foo"));
+    BOOST_TEST(!parser_.optional<std::string>("boolean", "foo"));
+    BOOST_TEST(!parser_.optional<std::string>("int", "foo"));
+    BOOST_TEST(!parser_.optional<std::string>("uint", "foo"));
 }
 
 BOOST_AUTO_TEST_SUITE_END()