changeset 88:469b6d558ab0

Common: add util::json functions, closes #628
author David Demelier <markand@malikania.fr>
date Sat, 04 Feb 2017 16:15:20 +0100
parents 955a9409ab5f
children 0bedc450a9d2
files libclient/malikania/client/loader.cpp libcommon/malikania/util.cpp libcommon/malikania/util.hpp tests/libcommon/util/main.cpp
diffstat 4 files changed, 514 insertions(+), 35 deletions(-) [+]
line wrap: on
line diff
--- a/libclient/malikania/client/loader.cpp	Sat Feb 04 14:40:20 2017 +0100
+++ b/libclient/malikania/client/loader.cpp	Sat Feb 04 16:15:20 2017 +0100
@@ -29,7 +29,7 @@
 using boost::str;
 using boost::format;
 
-using mlk::util::json::invalid;
+using namespace nlohmann;
 
 namespace mlk {
 
@@ -110,11 +110,7 @@
     auto sprite = load_sprite(require_string(id, value, "sprite"));
 
     // Load all frames.
-    auto property = value["frames"];
-
-    if (!property.is_array()) {
-        throw invalid(id, nlohmann::json::value_t::array);
-    }
+    auto property = util::json::require_array(value, "/frames"_json_pointer);
 
     animation_frames frames;
     int index = 0;
--- a/libcommon/malikania/util.cpp	Sat Feb 04 14:40:20 2017 +0100
+++ b/libcommon/malikania/util.cpp	Sat Feb 04 16:15:20 2017 +0100
@@ -16,6 +16,7 @@
  * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
  */
 
+#include <cassert>
 #include <string>
 #include <stdexcept>
 
@@ -263,38 +264,73 @@
     return result;
 }
 
+/*
+ * json utilities
+ * ------------------------------------------------------------------
+ */
+
 namespace json {
 
-std::runtime_error invalid(const std::string& name, const nlohmann::json::value_t type)
+/*
+ * XXX: until json get a non-throwing function to check if a pointer exists,
+ * use this to check for a value at the given pointer or throw a
+ * property_error.
+ */
+nlohmann::json require(const nlohmann::json& object,
+                       const nlohmann::json::json_pointer& pointer,
+                       const nlohmann::json::value_t expected)
 {
-    std::ostringstream oss;
-
-    oss << "invalid '" << name << "' property ('";
+    assert(object.is_object());
 
-    switch (type) {
-    case nlohmann::json::value_t::null:
-        oss << "null";
-        break;
-    case nlohmann::json::value_t::object:
-        oss << "object";
-        break;
-    case nlohmann::json::value_t::array:
-        oss << "array";
-        break;
-    case nlohmann::json::value_t::string:
-        oss << "string";
-        break;
-    case nlohmann::json::value_t::boolean:
-        oss << "boolean";
-        break;
-    default:
-        oss << "number";
-        break;
+    nlohmann::json value;
+
+    try {
+        value = object.at(pointer);
+    } catch (...) {
+        throw property_error(object, pointer, nlohmann::json::value_t::null, expected);
+    }
+
+    if (value.type() != expected) {
+        throw property_error(object, pointer, value.type(), expected);
     }
 
-    oss << " expected)";
+    return value;
+}
+
+nlohmann::json require_array(const nlohmann::json& object,
+                             const nlohmann::json::json_pointer& pointer)
+{
+    return require(object, pointer, nlohmann::json::value_t::array);
+}
+
+bool require_bool(const nlohmann::json& object,
+                  const nlohmann::json::json_pointer& pointer)
+{
+    return require(object, pointer, nlohmann::json::value_t::boolean);
+}
 
-    return std::runtime_error(oss.str());
+int require_int(const nlohmann::json& object,
+                const nlohmann::json::json_pointer& pointer)
+{
+    return require(object, pointer, nlohmann::json::value_t::number_integer);
+}
+
+nlohmann::json require_object(const nlohmann::json& object,
+                              const nlohmann::json::json_pointer& pointer)
+{
+    return require(object, pointer, nlohmann::json::value_t::object);
+}
+
+std::string require_string(const nlohmann::json& object,
+                           const nlohmann::json::json_pointer& pointer)
+{
+    return require(object, pointer, nlohmann::json::value_t::string);
+}
+
+unsigned require_uint(const nlohmann::json& object,
+                      const nlohmann::json::json_pointer& pointer)
+{
+    return require(object, pointer, nlohmann::json::value_t::number_unsigned);
 }
 
 } // !json
--- a/libcommon/malikania/util.hpp	Sat Feb 04 14:40:20 2017 +0100
+++ b/libcommon/malikania/util.hpp	Sat Feb 04 16:15:20 2017 +0100
@@ -25,7 +25,7 @@
  */
 
 #include <algorithm>
-#include <stdexcept>
+#include <exception>
 #include <string>
 #include <vector>
 
@@ -94,11 +94,179 @@
 namespace json {
 
 /**
- * Create an exception with a message like:
+ * \brief Indicates a property error in an object.
+ */
+class property_error : public std::exception {
+private:
+    nlohmann::json m_object;
+    nlohmann::json::json_pointer m_pointer;
+    nlohmann::json::value_t m_type;
+    nlohmann::json::value_t m_expected;
+    std::string m_message;
+
+public:
+    /**
+     * Construct the property error.
+     *
+     * \param object the faulty object
+     * \param pointer the property path
+     * \param type the found type
+     * \param expected the expected type
+     */
+    inline property_error(nlohmann::json object,
+                          nlohmann::json::json_pointer pointer,
+                          nlohmann::json::value_t type,
+                          nlohmann::json::value_t expected) noexcept
+        : m_object(std::move(object))
+        , m_pointer(std::move(pointer))
+        , m_type(type)
+        , m_expected(expected)
+    {
+        m_message += "invalid '" + m_pointer.to_string() + "' property ";
+        m_message += "(" + nlohmann::json(expected).type_name();
+        m_message += " expected, got ";
+        m_message += nlohmann::json(type).type_name() + ")";
+    }
+
+    /**
+     * Get the faulty object.
+     *
+     * return the object
+     */
+    inline const nlohmann::json& object() const noexcept
+    {
+        return m_object;
+    }
+
+    /**
+     * Get the pointer to the property.
+     *
+     * \return the pointer
+     */
+    inline const nlohmann::json::json_pointer& pointer() const noexcept
+    {
+        return m_pointer;
+    }
+
+    /**
+     * Get the actual type.
+     *
+     * \return the type
+     */
+    inline nlohmann::json::value_t type() const noexcept
+    {
+        return m_type;
+    }
+
+    /**
+     * Get the expected type.
+     *
+     * \return the type
+     */
+    inline nlohmann::json::value_t expected() const noexcept
+    {
+        return m_expected;
+    }
+
+    /**
+     * Get a human formatted message.
+     *
+     * \return the message
+     */
+    const char* what() const noexcept
+    {
+        return m_message.c_str();
+    }
+};
+
+/**
+ * Require a value at the given pointer, if the value does not match the
+ * expected type, an exception is thrown.
  *
- * 'invalid '<name>' property ('<type>' expected)
+ * If the property does not exists, a property_error with a null found type is
+ * thrown.
+ *
+ * \pre object.is_object()
+ * \param object the object
+ * \param pointer the pointer to the property
+ * \param expected the expected type
+ * \throw property_error on errors
+ */
+nlohmann::json require(const nlohmann::json& object,
+                       const nlohmann::json::json_pointer& pointer,
+                       const nlohmann::json::value_t expected);
+
+/**
+ * Require an array at the given pointer.
+ *
+ * \pre object.is_object()
+ * \param object the object
+ * \param pointer the pointer to the property
+ * \return the value
+ * \throw property_error on errors
+ */
+nlohmann::json require_array(const nlohmann::json& object,
+                             const nlohmann::json::json_pointer& pointer);
+
+/**
+ * Require a boolean at the given pointer.
+ *
+ * \pre object.is_object()
+ * \param object the object
+ * \param pointer the pointer to the property
+ * \return the value
+ * \throw property_error on errors
  */
-std::runtime_error invalid(const std::string& name, const nlohmann::json::value_t type);
+bool require_bool(const nlohmann::json& object,
+                  const nlohmann::json::json_pointer& pointer);
+
+/**
+ * Require an integer at the given pointer.
+ *
+ * \pre object.is_object()
+ * \param object the object
+ * \param pointer the pointer to the property
+ * \return the value
+ * \throw property_error on errors
+ */
+int require_int(const nlohmann::json& object,
+                const nlohmann::json::json_pointer& pointer);
+
+/**
+ * Require an object at the given pointer.
+ *
+ * \pre object.is_object()
+ * \param object the object
+ * \param pointer the pointer to the property
+ * \return the value
+ * \throw property_error on errors
+ */
+nlohmann::json require_object(const nlohmann::json& object,
+                              const nlohmann::json::json_pointer& pointer);
+
+/**
+ * Require a string at the given pointer.
+ *
+ * \pre object.is_object()
+ * \param object the object
+ * \param pointer the pointer to the property
+ * \return the value
+ * \throw property_error on errors
+ */
+std::string require_string(const nlohmann::json& object,
+                           const nlohmann::json::json_pointer& pointer);
+
+/**
+ * Require an unsigned integer at the given pointer.
+ *
+ * \pre object.is_object()
+ * \param object the object
+ * \param pointer the pointer to the property
+ * \return the value
+ * \throw property_error on errors
+ */
+unsigned require_uint(const nlohmann::json& object,
+                      const nlohmann::json::json_pointer& pointer);
 
 } // !json
 
--- a/tests/libcommon/util/main.cpp	Sat Feb 04 14:40:20 2017 +0100
+++ b/tests/libcommon/util/main.cpp	Sat Feb 04 16:15:20 2017 +0100
@@ -22,6 +22,21 @@
 #include <malikania/util.hpp>
 
 using namespace mlk;
+using namespace nlohmann;
+
+namespace nlohmann {
+
+namespace detail {
+
+std::ostream& operator<<(std::ostream& out, json::value_t type)
+{
+    out << json(type).type_name();
+    return out;
+}
+
+} // !detail
+
+} // !nlohmann
 
 /*
  * util::clamp
@@ -56,3 +71,267 @@
 }
 
 BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * util::json::require
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(json_require)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    json object{
+        { "b", true },
+        { "i", 123 },
+        { "s", "blabla" }
+    };
+
+    BOOST_REQUIRE_EQUAL(util::json::require(
+        object, "/b"_json_pointer, json::value_t::boolean).type(), json::value_t::boolean);
+    BOOST_REQUIRE_EQUAL(util::json::require(
+        object, "/i"_json_pointer, json::value_t::number_integer).type(), json::value_t::number_integer);
+    BOOST_REQUIRE_EQUAL(util::json::require(
+        object, "/s"_json_pointer, json::value_t::string).type(), json::value_t::string);
+}
+
+BOOST_AUTO_TEST_CASE(nonexistent)
+{
+    auto json = json::object();
+
+    try {
+        util::json::require(json, "/non-existent"_json_pointer, json::value_t::string);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::null);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::string);
+    }
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    json object{
+        { "not-string", 123 }
+    };
+
+    try {
+        util::json::require(object, "/not-string"_json_pointer, json::value_t::string);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::number_integer);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::string);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * util::json::require_array
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(json_require_array)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    json object{
+        { "a", { 1, 2, 3 } },
+        { "l1", {
+                { "l2", { 4, 5, 6 } }
+            }
+        }
+    };
+
+    auto a = util::json::require_array(object, "/a"_json_pointer);
+    auto l2 = util::json::require_array(object, "/l1/l2"_json_pointer);
+
+    BOOST_REQUIRE(a.is_array());
+    BOOST_REQUIRE(l2.is_array());
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    json object{
+        { "not-array", 123 }
+    };
+
+    try {
+        util::json::require_array(object, "/not-array"_json_pointer);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::number_integer);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::array);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * util::json::require_bool
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(json_require_bool)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    json object{
+        { "b", true }
+    };
+
+    BOOST_REQUIRE_EQUAL(util::json::require_bool(object, "/b"_json_pointer), true);
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    json object{
+        { "not-bool", 123 }
+    };
+
+    try {
+        util::json::require_bool(object, "/not-bool"_json_pointer);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::number_integer);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::boolean);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * util::json::require_int
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(json_require_int)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    json object{
+        { "i", 123 }
+    };
+
+    BOOST_REQUIRE_EQUAL(util::json::require_int(object, "/i"_json_pointer), 123);
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    json object{
+        { "not-int", true }
+    };
+
+    try {
+        util::json::require_int(object, "/not-int"_json_pointer);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::boolean);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::number_integer);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * util::json::require_object
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(json_require_object)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    json object{
+        {
+            "network", json::object({
+                { "host", "localhost" },
+                { "port", 9090 }
+            })
+        }
+    };
+
+    BOOST_REQUIRE(util::json::require_object(object, "/network"_json_pointer).is_object());
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    json object{
+        { "not-object", 123 }
+    };
+
+    try {
+        util::json::require_object(object, "/not-object"_json_pointer);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::number_integer);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::object);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * util::json::require_string
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(json_require_string)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    json object{
+        { "s", "hello" }
+    };
+
+    BOOST_REQUIRE_EQUAL(util::json::require_string(object, "/s"_json_pointer), "hello");
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    json object{
+        { "not-string", 123 }
+    };
+
+    try {
+        util::json::require_string(object, "/not-string"_json_pointer);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::number_integer);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::string);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()
+
+/*
+ * util::json::require_uint
+ * ------------------------------------------------------------------
+ */
+
+BOOST_AUTO_TEST_SUITE(json_require_uint)
+
+BOOST_AUTO_TEST_CASE(simple)
+{
+    json object{
+        { "u1", 123U }
+    };
+
+    BOOST_REQUIRE_EQUAL(util::json::require_uint(object, "/u1"_json_pointer), 123U);
+}
+
+BOOST_AUTO_TEST_CASE(invalid)
+{
+    json object{
+        { "not-uint", true }
+    };
+
+    try {
+        util::json::require_uint(object, "/not-uint"_json_pointer);
+        BOOST_FAIL("exception expected");
+    } catch (const util::json::property_error& ex) {
+        BOOST_REQUIRE_EQUAL(ex.type(), json::value_t::boolean);
+        BOOST_REQUIRE_EQUAL(ex.expected(), json::value_t::number_unsigned);
+    }
+}
+
+BOOST_AUTO_TEST_SUITE_END()