Allow parsing null or value classes

In Redfish schema, just about all values can be a type (string,
EDM.Numeric, etc) or null.  Most APIs don't allow explicitly setting
null, but there are a few cases where it is useful, namely in lists,
where an an empty object {} keeps the value the same, and null deletes
the value from the list.

Previously we handled this by unpacking as nlohmann::json, but this
allowed things like

[1.0, {}] to pass the check for an array of string values.  We'd
ideally like to reject the 1.0 at the first stage, as well as reduce
the number of tiered readJson calls that we make.

This commit introducess support for unpacking std::variant types, that
allows unpacking a known type, or explicitly allowing null, by unpacking
std::nullptr_t.

Tested: Unit tests pass.

Change-Id: Ic7451877c824ac743faf1951cc2b5d9f8df8019c
Signed-off-by: Ed Tanous <edtanous@google.com>
diff --git a/redfish-core/include/utils/json_utils.hpp b/redfish-core/include/utils/json_utils.hpp
index 0f4ba06..feb4e54 100644
--- a/redfish-core/include/utils/json_utils.hpp
+++ b/redfish-core/include/utils/json_utils.hpp
@@ -90,6 +90,14 @@
 struct IsStdArray<std::array<Type, size>> : std::true_type
 {};
 
+template <typename Type>
+struct IsVariant : std::false_type
+{};
+
+template <typename... Types>
+struct IsVariant<std::variant<Types...>> : std::true_type
+{};
+
 enum class UnpackErrorCode
 {
     success,
@@ -126,6 +134,29 @@
 
 template <typename Type>
 UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue,
+                                         std::string_view key, Type& value);
+
+template <std::size_t Index = 0, typename... Args>
+UnpackErrorCode unpackValueVariant(nlohmann::json& j, std::string_view key,
+                                   std::variant<Args...>& v)
+{
+    if constexpr (Index < std::variant_size_v<std::variant<Args...>>)
+    {
+        std::variant_alternative_t<Index, std::variant<Args...>> type;
+        UnpackErrorCode unpack = unpackValueWithErrorCode(j, key, type);
+        if (unpack == UnpackErrorCode::success)
+        {
+            v = std::move(type);
+            return unpack;
+        }
+
+        return unpackValueVariant<Index + 1, Args...>(j, key, v);
+    }
+    return UnpackErrorCode::invalidType;
+}
+
+template <typename Type>
+UnpackErrorCode unpackValueWithErrorCode(nlohmann::json& jsonValue,
                                          std::string_view key, Type& value)
 {
     UnpackErrorCode ret = UnpackErrorCode::success;
@@ -188,6 +219,13 @@
     {
         value = std::move(jsonValue);
     }
+    else if constexpr (std::is_same_v<std::nullptr_t, Type>)
+    {
+        if (!jsonValue.is_null())
+        {
+            return UnpackErrorCode::invalidType;
+        }
+    }
     else
     {
         using JsonType = std::add_const_t<std::add_pointer_t<Type>>;
@@ -252,6 +290,22 @@
                   ret;
         }
     }
+    else if constexpr (IsVariant<Type>::value)
+    {
+        UnpackErrorCode ec = unpackValueVariant(jsonValue, key, value);
+        if (ec != UnpackErrorCode::success)
+        {
+            if (ec == UnpackErrorCode::invalidType)
+            {
+                messages::propertyValueTypeError(res, jsonValue, key);
+            }
+            else if (ec == UnpackErrorCode::outOfRange)
+            {
+                messages::propertyValueNotInList(res, jsonValue, key);
+            }
+            return false;
+        }
+    }
     else
     {
         UnpackErrorCode ec = unpackValueWithErrorCode(jsonValue, key, value);
@@ -342,6 +396,16 @@
     std::string*,
     nlohmann::json*,
     nlohmann::json::object_t*,
+    std::variant<std::string, std::nullptr_t>*,
+    std::variant<uint8_t, std::nullptr_t>*,
+    std::variant<int16_t, std::nullptr_t>*,
+    std::variant<uint16_t, std::nullptr_t>*,
+    std::variant<int32_t, std::nullptr_t>*,
+    std::variant<uint32_t, std::nullptr_t>*,
+    std::variant<int64_t, std::nullptr_t>*,
+    std::variant<uint64_t, std::nullptr_t>*,
+    std::variant<double, std::nullptr_t>*,
+    std::variant<bool, std::nullptr_t>*,
     std::vector<uint8_t>*,
     std::vector<uint16_t>*,
     std::vector<int16_t>*,
@@ -377,7 +441,19 @@
     std::optional<std::vector<double>>*,
     std::optional<std::vector<std::string>>*,
     std::optional<std::vector<nlohmann::json>>*,
-    std::optional<std::vector<nlohmann::json::object_t>>*
+    std::optional<std::vector<nlohmann::json::object_t>>*,
+    std::optional<std::variant<std::string, std::nullptr_t>>*,
+    std::optional<std::variant<uint8_t, std::nullptr_t>>*,
+    std::optional<std::variant<int16_t, std::nullptr_t>>*,
+    std::optional<std::variant<uint16_t, std::nullptr_t>>*,
+    std::optional<std::variant<int32_t, std::nullptr_t>>*,
+    std::optional<std::variant<uint32_t, std::nullptr_t>>*,
+    std::optional<std::variant<int64_t, std::nullptr_t>>*,
+    std::optional<std::variant<uint64_t, std::nullptr_t>>*,
+    std::optional<std::variant<double, std::nullptr_t>>*,
+    std::optional<std::variant<bool, std::nullptr_t>>*,
+    std::optional<std::vector<std::variant<nlohmann::json::object_t, std::nullptr_t>>>*,
+    std::optional<std::variant<nlohmann::json::object_t, std::nullptr_t>>*
 >;
 // clang-format on
 
diff --git a/test/redfish-core/include/utils/json_utils_test.cpp b/test/redfish-core/include/utils/json_utils_test.cpp
index 5ef80ee..cf59297 100644
--- a/test/redfish-core/include/utils/json_utils_test.cpp
+++ b/test/redfish-core/include/utils/json_utils_test.cpp
@@ -6,10 +6,12 @@
 #include <boost/beast/http/status.hpp>
 #include <nlohmann/json.hpp>
 
+#include <cstddef>
 #include <cstdint>
 #include <optional>
 #include <string>
 #include <system_error>
+#include <variant>
 #include <vector>
 
 #include <gmock/gmock.h> // IWYU pragma: keep
@@ -70,6 +72,33 @@
     EXPECT_THAT(vec, ElementsAre(1, 2, 3));
 }
 
+TEST(ReadJson, VariantValueUnpackedNull)
+{
+    crow::Response res;
+    nlohmann::json jsonRequest = {{"nullval", nullptr}};
+
+    std::variant<std::string, std::nullptr_t> str;
+
+    ASSERT_TRUE(readJson(jsonRequest, res, "nullval", str));
+    EXPECT_EQ(res.result(), boost::beast::http::status::ok);
+
+    EXPECT_TRUE(std::holds_alternative<std::nullptr_t>(str));
+}
+
+TEST(ReadJson, VariantValueUnpackedValue)
+{
+    crow::Response res;
+    nlohmann::json jsonRequest = {{"stringval", "mystring"}};
+
+    std::variant<std::string, std::nullptr_t> str;
+
+    ASSERT_TRUE(readJson(jsonRequest, res, "stringval", str));
+    EXPECT_EQ(res.result(), boost::beast::http::status::ok);
+
+    ASSERT_TRUE(std::holds_alternative<std::string>(str));
+    EXPECT_EQ(std::get<std::string>(str), "mystring");
+}
+
 TEST(readJson, ExtraElementsReturnsFalseReponseIsBadRequest)
 {
     crow::Response res;