Persistency: store special double values

Json library handles infinity and NaN as null. During deserialization,
we expect a double and those files were marked as invalid and removed.
To prevent this behavior and preserve all double values, LabeledTuple
'to_json' and 'from_json' functions were improved to accommodate those
special values by using string literals.

Testing done:
- UTs added for double<=>json conversion.
- UTs passing.

Signed-off-by: Szymon Dompke <szymon.dompke@intel.com>
Change-Id: I9193df29cce1db28cda3c895d117d9f3bfca2c24
diff --git a/src/utils/labeled_tuple.hpp b/src/utils/labeled_tuple.hpp
index e6203dd..c4120c9 100644
--- a/src/utils/labeled_tuple.hpp
+++ b/src/utils/labeled_tuple.hpp
@@ -3,9 +3,19 @@
 #include <nlohmann/json.hpp>
 #include <sdbusplus/message/types.hpp>
 
+#include <cmath>
+#include <limits>
+
 namespace utils
 {
 
+namespace numeric_literals
+{
+constexpr std::string_view NaN = "NaN";
+constexpr std::string_view infinity = "inf";
+constexpr std::string_view infinity_negative = "-inf";
+} // namespace numeric_literals
+
 inline void from_json(const nlohmann::json& j,
                       sdbusplus::message::object_path& o)
 {
@@ -22,6 +32,54 @@
     }
 }
 
+inline void to_json(nlohmann::json& j, const double& val)
+{
+    if (std::isnan(val))
+    {
+        j = numeric_literals::NaN;
+    }
+    else if (val == std::numeric_limits<double>::infinity())
+    {
+        j = numeric_literals::infinity;
+    }
+    else if (val == -std::numeric_limits<double>::infinity())
+    {
+        j = numeric_literals::infinity_negative;
+    }
+    else
+    {
+        j = val;
+    }
+}
+
+inline void from_json(const nlohmann::json& j, double& val)
+{
+    if (j.is_number())
+    {
+        val = j.get<double>();
+    }
+    else
+    {
+        auto str_val = j.get<std::string>();
+        if (str_val == numeric_literals::NaN)
+        {
+            val = std::numeric_limits<double>::quiet_NaN();
+        }
+        else if (str_val == numeric_literals::infinity)
+        {
+            val = std::numeric_limits<double>::infinity();
+        }
+        else if (str_val == numeric_literals::infinity_negative)
+        {
+            val = -std::numeric_limits<double>::infinity();
+        }
+        else
+        {
+            throw std::invalid_argument("Unknown numeric literal");
+        }
+    }
+}
+
 namespace detail
 {
 
@@ -45,6 +103,38 @@
 template <class T>
 constexpr bool has_utils_from_json_v = has_utils_from_json<T>::value;
 
+template <class T>
+struct has_utils_to_json
+{
+    template <class U>
+    static U& ref();
+
+    template <class U>
+    static std::true_type
+        check(decltype(utils::to_json(ref<nlohmann::json>(), ref<const U>()))*);
+
+    template <class>
+    static std::false_type check(...);
+
+    static constexpr bool value =
+        decltype(check<std::decay_t<T>>(nullptr))::value;
+};
+
+template <class T>
+constexpr bool has_utils_to_json_v = has_utils_to_json<T>::value;
+
+bool eq(const auto& a, const auto& b)
+{
+    if constexpr (std::is_same<std::decay_t<decltype(a)>, double>())
+    {
+        if (std::isnan(a))
+        {
+            return std::isnan(b);
+        }
+    }
+    return a == b;
+}
+
 } // namespace detail
 
 template <class, class...>
@@ -117,7 +207,15 @@
 
     bool operator==(const LabeledTuple& other) const
     {
-        return value == other.value;
+        return std::apply(
+            [&](auto&&... x) {
+                return std::apply(
+                    [&](auto&&... y) {
+                        return (true && ... && detail::eq(x, y));
+                    },
+                    value);
+            },
+            other.value);
     }
 
     bool operator<(const LabeledTuple& other) const
@@ -136,7 +234,16 @@
     void to_json_item(nlohmann::json& j) const
     {
         using Label = std::tuple_element_t<Idx, std::tuple<Labels...>>;
-        j[Label::str()] = std::get<Idx>(value);
+        using T = std::tuple_element_t<Idx, tuple_type>;
+        nlohmann::json& item = j[Label::str()];
+        if constexpr (detail::has_utils_to_json_v<T>)
+        {
+            utils::to_json(item, std::get<Idx>(value));
+        }
+        else
+        {
+            item = std::get<Idx>(value);
+        }
     }
 
     template <size_t... Idx>
diff --git a/tests/meson.build b/tests/meson.build
index ce86dd3..29aaeb0 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -40,6 +40,7 @@
             'src/test_detached_timer.cpp',
             'src/test_discrete_threshold.cpp',
             'src/test_ensure.cpp',
+            'src/test_labeled_tuple.cpp',
             'src/test_make_id_name.cpp',
             'src/test_metric.cpp',
             'src/test_numeric_threshold.cpp',
diff --git a/tests/src/test_labeled_tuple.cpp b/tests/src/test_labeled_tuple.cpp
new file mode 100644
index 0000000..85f2edb
--- /dev/null
+++ b/tests/src/test_labeled_tuple.cpp
@@ -0,0 +1,84 @@
+#include "helpers.hpp"
+#include "utils/labeled_tuple.hpp"
+
+#include <iostream>
+#include <limits>
+
+#include <gmock/gmock.h>
+
+using namespace testing;
+
+struct TestingLabelDouble
+{
+    static std::string str()
+    {
+        return "DoubleValue";
+    }
+};
+
+struct TestingLabelString
+{
+    static std::string str()
+    {
+        return "StringValue";
+    }
+};
+
+using LabeledTestingTuple =
+    utils::LabeledTuple<std::tuple<double, std::string>, TestingLabelDouble,
+                        TestingLabelString>;
+
+class TestLabeledTupleDoubleSpecialization :
+    public Test,
+    public WithParamInterface<
+        std::tuple<double, std::variant<double, std::string>>>
+{
+  public:
+    const std::string string_value = "Some value";
+};
+
+TEST_P(TestLabeledTupleDoubleSpecialization,
+       serializeAndDeserializeMakesSameTuple)
+{
+    auto [double_value, expected_serialized_value] = GetParam();
+    LabeledTestingTuple initial(double_value, string_value);
+    nlohmann::json serialized(initial);
+
+    EXPECT_EQ(serialized["StringValue"], string_value);
+
+    auto& actual_serialized_value = serialized["DoubleValue"];
+    if (std::holds_alternative<std::string>(expected_serialized_value))
+    {
+        EXPECT_TRUE(actual_serialized_value.is_string());
+        EXPECT_EQ(actual_serialized_value.get<std::string>(),
+                  std::get<std::string>(expected_serialized_value));
+    }
+    else
+    {
+        EXPECT_TRUE(actual_serialized_value.is_number());
+        EXPECT_EQ(actual_serialized_value.get<double>(),
+                  std::get<double>(expected_serialized_value));
+    }
+
+    LabeledTestingTuple deserialized = serialized.get<LabeledTestingTuple>();
+    EXPECT_EQ(initial, deserialized);
+}
+
+INSTANTIATE_TEST_SUITE_P(
+    _, TestLabeledTupleDoubleSpecialization,
+    Values(std::make_tuple(10.0, std::variant<double, std::string>(10.0)),
+           std::make_tuple(std::numeric_limits<double>::infinity(),
+                           std::variant<double, std::string>("inf")),
+           std::make_tuple(-std::numeric_limits<double>::infinity(),
+                           std::variant<double, std::string>("-inf")),
+           std::make_tuple(std::numeric_limits<double>::quiet_NaN(),
+                           std::variant<double, std::string>("NaN"))));
+
+TEST(TestLabeledTupleDoubleSpecializationNegative,
+     ThrowsWhenUnknownLiteralDuringDeserialization)
+{
+    nlohmann::json data = nlohmann::json{{"DoubleValue", "FooBar"},
+                                         {"StringValue", "Some Text Val"}};
+
+    EXPECT_THROW(data.get<LabeledTestingTuple>(), std::invalid_argument);
+}