Added utility functions getAllProperties and unpackProperties

Tested:
  - Added example to verify that functions work correctly
  - Added new unit tests that are passing
  - All other tests are still passing after this change
  - Added handling of new type (std::monostate) which can
    be used as first type in variant to represent that none
    of the other types was matched

Change-Id: Ic8e7c8d3116d64b94be37147ae8a80ebb5d3811d
Signed-off-by: Krzysztof Grobelny <krzysztof.grobelny@intel.com>
diff --git a/example/get-all-properties.cpp b/example/get-all-properties.cpp
new file mode 100644
index 0000000..8e7ee54
--- /dev/null
+++ b/example/get-all-properties.cpp
@@ -0,0 +1,228 @@
+#include <boost/asio.hpp>
+#include <sdbusplus/asio/connection.hpp>
+#include <sdbusplus/asio/get_all_properties.hpp>
+#include <sdbusplus/asio/object_server.hpp>
+#include <sdbusplus/bus.hpp>
+#include <sdbusplus/unpack_properties.hpp>
+
+#include <iostream>
+
+namespace xyz
+{
+namespace demo
+{
+
+const std::string path = "/xyz/demo";
+const std::string name = "xyz.demo";
+const std::string interface = "xyz.demo.interface";
+
+} // namespace demo
+} // namespace xyz
+
+namespace name
+{
+
+const std::string greetings = "Greetings";
+const std::string goodbyes = "Goodbyes";
+const std::string value = "Value";
+
+} // namespace name
+
+class Application
+{
+  public:
+    Application(boost::asio::io_context& ioc, sdbusplus::asio::connection& bus,
+                sdbusplus::asio::object_server& objServer) :
+        ioc_(ioc),
+        bus_(bus), objServer_(objServer)
+    {
+        demo_ = objServer_.add_interface(xyz::demo::path, xyz::demo::interface);
+
+        demo_->register_property_r(name::greetings, std::string(),
+                                   sdbusplus::vtable::property_::const_,
+                                   [this](const auto&) { return greetings_; });
+
+        demo_->register_property_rw(
+            name::goodbyes, std::string(),
+            sdbusplus::vtable::property_::emits_change,
+            [this](const auto& newPropertyValue, const auto&) {
+                goodbyes_ = newPropertyValue;
+                return 1;
+            },
+            [this](const auto&) { return goodbyes_; });
+
+        demo_->register_property_r(
+            name::value, uint32_t{42}, sdbusplus::vtable::property_::const_,
+            [](const auto& value) -> uint32_t { return value; });
+
+        demo_->initialize();
+    }
+
+    ~Application()
+    {
+        objServer_.remove_interface(demo_);
+    }
+
+    uint32_t fatalErrors() const
+    {
+        return fatalErrors_;
+    }
+
+    auto logSystemErrorCode()
+    {
+        return [this](boost::system::error_code ec) {
+            std::cerr << "Error: " << ec << "\n";
+            ++fatalErrors_;
+        };
+    }
+
+    void logException(const std::exception& e)
+    {
+        std::cerr << "Error: " << e.what() << "\n";
+        ++fatalErrors_;
+    }
+
+    void logExpectedException(
+        const sdbusplus::exception::UnpackPropertyError& error)
+    {
+        std::cout << "As expected " << error.what() << " => "
+                  << error.propertyName << " is missing because "
+                  << error.reason << "\n";
+    }
+
+    void asyncGetAllPropertiesStringTypeOnly()
+    {
+        sdbusplus::asio::getAllProperties(
+            bus_, xyz::demo::name, xyz::demo::path, xyz::demo::interface,
+            logSystemErrorCode(),
+            [this](std::vector<std::pair<
+                       std::string, std::variant<std::monostate, std::string>>>&
+                       properties) {
+                try
+                {
+                    std::string greetings;
+                    std::string goodbyes;
+                    sdbusplus::unpackProperties(properties, name::greetings,
+                                                greetings, name::goodbyes,
+                                                goodbyes);
+
+                    std::cout << "value of greetings: " << greetings << "\n";
+                    std::cout << "value of goodbyes: " << goodbyes << "\n";
+                }
+                catch (const sdbusplus::exception::UnpackPropertyError& error)
+                {
+                    logException(error);
+                }
+
+                try
+                {
+                    std::string value;
+                    sdbusplus::unpackProperties(properties, name::value, value);
+
+                    std::cerr << "Error: it should fail because of "
+                                 "not matched type\n";
+                    ++fatalErrors_;
+                }
+                catch (const sdbusplus::exception::UnpackPropertyError& error)
+                {
+                    logExpectedException(error);
+                }
+            });
+    }
+
+    void asyncGetAllProperties()
+    {
+        sdbusplus::asio::getAllProperties(
+            bus_, xyz::demo::name, xyz::demo::path, xyz::demo::interface,
+            logSystemErrorCode(),
+            [this](
+                std::vector<std::pair<std::string,
+                                      std::variant<std::monostate, std::string,
+                                                   uint32_t>>>& properties) {
+                try
+                {
+                    std::string greetings;
+                    std::string goodbyes;
+                    uint32_t value = 0u;
+                    sdbusplus::unpackProperties(properties, name::greetings,
+                                                greetings, name::goodbyes,
+                                                goodbyes, name::value, value);
+
+                    std::cout << "value of greetings: " << greetings << "\n";
+                    std::cout << "value of goodbyes: " << goodbyes << "\n";
+                    std::cout << "value of value: " << value << "\n";
+                }
+                catch (const sdbusplus::exception::UnpackPropertyError& error)
+                {
+                    logException(error);
+                }
+
+                try
+                {
+                    std::string unknownProperty;
+                    sdbusplus::unpackProperties(
+                        properties, "UnknownPropertyName", unknownProperty);
+
+                    std::cerr << "Error: it should fail because of "
+                                 "missing property\n";
+                    ++fatalErrors_;
+                }
+                catch (const sdbusplus::exception::UnpackPropertyError& error)
+                {
+                    logExpectedException(error);
+                }
+
+                try
+                {
+                    uint32_t notMatchingType;
+                    sdbusplus::unpackProperties(properties, name::greetings,
+                                                notMatchingType);
+
+                    std::cerr << "Error: it should fail because of "
+                                 "not matched type\n";
+                    ++fatalErrors_;
+                }
+                catch (const sdbusplus::exception::UnpackPropertyError& error)
+                {
+                    logExpectedException(error);
+                }
+            });
+    }
+
+  private:
+    boost::asio::io_context& ioc_;
+    sdbusplus::asio::connection& bus_;
+    sdbusplus::asio::object_server& objServer_;
+
+    std::shared_ptr<sdbusplus::asio::dbus_interface> demo_;
+    std::string greetings_ = "Hello";
+    std::string goodbyes_ = "Bye";
+
+    uint32_t fatalErrors_ = 0u;
+};
+
+int main(int, char**)
+{
+    boost::asio::io_context ioc;
+    boost::asio::signal_set signals(ioc, SIGINT, SIGTERM);
+
+    signals.async_wait(
+        [&ioc](const boost::system::error_code&, const int&) { ioc.stop(); });
+
+    auto bus = std::make_shared<sdbusplus::asio::connection>(ioc);
+    auto objServer = std::make_unique<sdbusplus::asio::object_server>(bus);
+
+    bus->request_name(xyz::demo::name.c_str());
+
+    Application app(ioc, *bus, *objServer);
+
+    boost::asio::post(ioc,
+                      [&app] { app.asyncGetAllPropertiesStringTypeOnly(); });
+    boost::asio::post(ioc, [&app] { app.asyncGetAllProperties(); });
+
+    ioc.run();
+
+    std::cout << "Fatal errors count: " << app.fatalErrors() << "\n";
+
+    return app.fatalErrors();
+}
diff --git a/example/meson.build b/example/meson.build
index aed2a33..5a6606e 100644
--- a/example/meson.build
+++ b/example/meson.build
@@ -35,6 +35,19 @@
     dependencies: [ boost_dep, sdbusplus_dep ],
 )
 
+executable(
+    'get-all-properties',
+    'get-all-properties.cpp',
+    cpp_args: [
+        '-DBOOST_ASIO_DISABLE_THREADS',
+        '-DBOOST_ALL_NO_LIB',
+        '-DBOOST_SYSTEM_NO_DEPRECATED',
+        '-DBOOST_ERROR_CODE_HEADER_ONLY',
+        '-DBOOST_COROUTINES_NO_DEPRECATION_WARNING',
+    ],
+    dependencies: [ boost_dep, sdbusplus_dep ],
+)
+
 calc_buildroot = meson.current_build_dir()
 calc_files = files(
     run_command(
diff --git a/include/sdbusplus/asio/get_all_properties.hpp b/include/sdbusplus/asio/get_all_properties.hpp
new file mode 100644
index 0000000..6bc41fe
--- /dev/null
+++ b/include/sdbusplus/asio/get_all_properties.hpp
@@ -0,0 +1,34 @@
+#pragma once
+
+#include <sdbusplus/asio/connection.hpp>
+
+namespace sdbusplus::asio
+{
+
+template <typename OnError, typename OnSuccess>
+inline void getAllProperties(sdbusplus::asio::connection& bus,
+                             const std::string& service,
+                             const std::string& path,
+                             const std::string& interface, OnError&& onError,
+                             OnSuccess&& onSuccess)
+{
+    using FunctionTuple = boost::callable_traits::args_t<OnSuccess>;
+    using FunctionTupleType =
+        typename sdbusplus::utility::decay_tuple<FunctionTuple>::type;
+
+    bus.async_method_call(
+        [onError = std::move(onError), onSuccess = std::move(onSuccess)](
+            boost::system::error_code ec,
+            std::tuple_element_t<0, FunctionTupleType>& ret) {
+            if (ec)
+            {
+                onError(ec);
+                return;
+            }
+
+            onSuccess(ret);
+        },
+        service, path, "org.freedesktop.DBus.Properties", "GetAll", interface);
+}
+
+} // namespace sdbusplus::asio
diff --git a/include/sdbusplus/exception.hpp b/include/sdbusplus/exception.hpp
index 98dc2da..1a83a09 100644
--- a/include/sdbusplus/exception.hpp
+++ b/include/sdbusplus/exception.hpp
@@ -80,6 +80,33 @@
     const char* what() const noexcept override;
 };
 
+/** Exception for when unpackProperties cannot find given property in provided
+ * container */
+class UnpackPropertyError final : public internal_exception
+{
+  public:
+    UnpackPropertyError(std::string_view propertyName, std::string_view reason);
+
+    static constexpr std::string_view reasonMissingProperty =
+        "Missing property";
+    static constexpr std::string_view reasonTypeNotMatched = "Type not matched";
+
+    static constexpr auto errName =
+        "xyz.openbmc_project.sdbusplus.Error.UnpackPropertyError";
+    static constexpr auto errDesc =
+        "unpackProperties failed to unpack one of requested properties.";
+    static constexpr auto errWhat =
+        "xyz.openbmc_project.sdbusplus.Error.UnpackPropertyError: "
+        "unpackProperties failed to unpack one of requested properties.";
+
+    const char* name() const noexcept override;
+    const char* description() const noexcept override;
+    const char* what() const noexcept override;
+
+    const std::string propertyName;
+    const std::string reason;
+};
+
 } // namespace exception
 
 using exception_t = exception::exception;
diff --git a/include/sdbusplus/message/read.hpp b/include/sdbusplus/message/read.hpp
index 9f58182..5024196 100644
--- a/include/sdbusplus/message/read.hpp
+++ b/include/sdbusplus/message/read.hpp
@@ -448,6 +448,15 @@
     }
 };
 
+/** @brief Specialization of read_single for std::monostate. */
+template <>
+struct read_single<std::monostate>
+{
+    template <typename S>
+    static void op(sdbusplus::SdBusInterface*, sd_bus_message*, S&&)
+    {}
+};
+
 template <typename T>
 static void tuple_item_read(sdbusplus::SdBusInterface* intf, sd_bus_message* m,
                             T&& t)
diff --git a/include/sdbusplus/message/types.hpp b/include/sdbusplus/message/types.hpp
index f45e1cd..6aaafd9 100644
--- a/include/sdbusplus/message/types.hpp
+++ b/include/sdbusplus/message/types.hpp
@@ -259,6 +259,12 @@
     constexpr static auto value = std::make_tuple('\0');
 };
 
+template <>
+struct type_id<std::monostate>
+{
+    constexpr static auto value = std::make_tuple('\0');
+};
+
 template <typename T>
 constexpr auto type_id_single()
 {
diff --git a/include/sdbusplus/unpack_properties.hpp b/include/sdbusplus/unpack_properties.hpp
new file mode 100644
index 0000000..c204337
--- /dev/null
+++ b/include/sdbusplus/unpack_properties.hpp
@@ -0,0 +1,138 @@
+#pragma once
+
+#include <sdbusplus/exception.hpp>
+#include <sdbusplus/utility/type_traits.hpp>
+
+#include <algorithm>
+#include <bitset>
+#include <optional>
+#include <stdexcept>
+#include <string>
+#include <string_view>
+#include <variant>
+
+namespace sdbusplus
+{
+namespace detail
+{
+
+template <typename Variant, typename ValueType>
+bool getIf(Variant&& variant, ValueType& outValue)
+{
+    if (auto value = std::get_if<ValueType>(&variant))
+    {
+        outValue = std::move(*value);
+        return true;
+    }
+
+    return false;
+}
+
+template <typename Container>
+auto findProperty(Container&& container, const std::string& key)
+{
+    if constexpr (utility::has_member_find_v<Container>)
+    {
+        return container.find(key);
+    }
+    else
+    {
+        return std::find_if(
+            std::begin(container), std::end(container),
+            [&key](const auto& keyValue) { return keyValue.first == key; });
+    }
+}
+
+template <typename Container>
+bool containsProperty(Container&& container, const std::string& key)
+{
+    if constexpr (utility::has_member_contains_v<Container>)
+    {
+        return container.contains(key);
+    }
+    else
+    {
+        return findProperty(std::forward<Container>(container), key) !=
+               std::end(container);
+    }
+}
+
+template <size_t Index, typename Container, size_t N, typename ValueType,
+          typename... Args>
+void readProperties(Container&& container, std::bitset<N>& assigned,
+                    const std::string& expectedKey, ValueType& outValue,
+                    Args&&... args)
+{
+    static_assert(Index < N);
+
+    auto it = findProperty(std::forward<Container>(container), expectedKey);
+
+    if (it != std::end(container))
+    {
+        if (getIf(it->second, outValue))
+        {
+            assigned.set(Index);
+        }
+    }
+
+    if constexpr (sizeof...(Args) > 0)
+    {
+        readProperties<Index + 1>(std::forward<Container>(container), assigned,
+                                  std::forward<Args>(args)...);
+    }
+}
+
+template <size_t Index, size_t N, typename ValueType, typename... Args>
+std::string findMissingProperty(std::bitset<N>& assigned,
+                                const std::string& key, ValueType&,
+                                Args&&... args)
+{
+    static_assert(Index < N);
+
+    if (!assigned.test(Index))
+    {
+        return key;
+    }
+
+    if constexpr (sizeof...(Args) > 0)
+    {
+        return findMissingProperty<Index + 1>(assigned,
+                                              std::forward<Args>(args)...);
+    }
+
+    return {};
+}
+
+} // namespace detail
+
+template <typename Container, typename... Args>
+void unpackProperties(Container&& input, Args&&... args)
+{
+    static_assert(sizeof...(Args) % 2 == 0);
+
+    auto assigned = std::bitset<sizeof...(Args) / 2>();
+
+    detail::readProperties<0>(input, assigned, std::forward<Args>(args)...);
+
+    if (!assigned.all())
+    {
+        std::string missingProperty = detail::findMissingProperty<0>(
+            assigned, std::forward<Args>(args)...);
+
+        if (detail::containsProperty(std::forward<Container>(input),
+                                     missingProperty))
+        {
+            throw exception::UnpackPropertyError(
+                missingProperty,
+                exception::UnpackPropertyError::reasonTypeNotMatched);
+        }
+        else
+        {
+            throw exception::UnpackPropertyError(
+                missingProperty,
+                exception::UnpackPropertyError::reasonMissingProperty);
+        }
+    }
+}
+
+} // namespace sdbusplus
diff --git a/include/sdbusplus/utility/type_traits.hpp b/include/sdbusplus/utility/type_traits.hpp
index 0d4f954..51f5081 100644
--- a/include/sdbusplus/utility/type_traits.hpp
+++ b/include/sdbusplus/utility/type_traits.hpp
@@ -91,6 +91,48 @@
     return strip_ends(s, std::make_index_sequence<N - 3>{});
 }
 
+template <typename T>
+class has_member_find
+{
+  private:
+    template <typename U>
+    static U& ref();
+
+    template <typename U>
+    static std::true_type check(decltype(ref<U>().find(
+        ref<std::tuple_element_t<0, typename U::value_type>>()))*);
+    template <typename>
+    static std::false_type check(...);
+
+  public:
+    static constexpr bool value =
+        decltype(check<std::decay_t<T>>(nullptr))::value;
+};
+
+template <typename T>
+constexpr bool has_member_find_v = has_member_find<T>::value;
+
+template <typename T>
+class has_member_contains
+{
+  private:
+    template <typename U>
+    static U& ref();
+
+    template <typename U>
+    static std::true_type check(decltype(ref<U>().contains(
+        ref<std::tuple_element_t<0, typename U::value_type>>()))*);
+    template <typename>
+    static std::false_type check(...);
+
+  public:
+    static constexpr bool value =
+        decltype(check<std::decay_t<T>>(nullptr))::value;
+};
+
+template <typename T>
+constexpr bool has_member_contains_v = has_member_contains<T>::value;
+
 } // namespace utility
 
 } // namespace sdbusplus
diff --git a/src/exception.cpp b/src/exception.cpp
index c13c5dd..17a988d 100644
--- a/src/exception.cpp
+++ b/src/exception.cpp
@@ -120,5 +120,26 @@
     return errWhat;
 }
 
+UnpackPropertyError::UnpackPropertyError(std::string_view propertyName,
+                                         std::string_view reason) :
+    propertyName(propertyName),
+    reason(reason)
+{}
+
+const char* UnpackPropertyError::name() const noexcept
+{
+    return errName;
+}
+
+const char* UnpackPropertyError::description() const noexcept
+{
+    return errDesc;
+}
+
+const char* UnpackPropertyError::what() const noexcept
+{
+    return errWhat;
+}
+
 } // namespace exception
 } // namespace sdbusplus
diff --git a/test/meson.build b/test/meson.build
index 374dada..02e4550 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -28,6 +28,7 @@
     'message/native_types',
     'message/types',
     'timer',
+    'unpack_properties',
     'utility/tuple_to_array',
     'utility/type_traits',
 ]
diff --git a/test/unpack_properties.cpp b/test/unpack_properties.cpp
new file mode 100644
index 0000000..90415cb
--- /dev/null
+++ b/test/unpack_properties.cpp
@@ -0,0 +1,193 @@
+#include <boost/container/flat_map.hpp>
+#include <sdbusplus/unpack_properties.hpp>
+
+#include <gmock/gmock.h>
+
+namespace sdbusplus
+{
+
+using VariantType = std::variant<std::string, uint32_t, float, double>;
+using ContainerTypes =
+    testing::Types<std::vector<std::pair<std::string, VariantType>>,
+                   boost::container::flat_map<std::string, VariantType>,
+                   std::map<std::string, VariantType>>;
+
+template <typename Exception, typename F>
+std::optional<Exception> captureException(F&& code)
+{
+    try
+    {
+        code();
+    }
+    catch (const Exception& e)
+    {
+        return e;
+    }
+
+    return std::nullopt;
+}
+
+template <typename Container>
+struct UnpackPropertiesTest : public testing::Test
+{
+    void SetUp() override
+    {
+        using namespace std::string_literals;
+
+        data.insert(data.end(),
+                    std::make_pair("Key-1"s, VariantType("string"s)));
+        data.insert(data.end(), std::make_pair("Key-2"s, VariantType(42.f)));
+        data.insert(data.end(), std::make_pair("Key-3"s, VariantType(15.)));
+    }
+
+    Container data;
+};
+
+TYPED_TEST_SUITE(UnpackPropertiesTest, ContainerTypes);
+
+TYPED_TEST(UnpackPropertiesTest, returnsValueWhenKeyIsPresentAndTypeMatches)
+{
+    using namespace testing;
+
+    std::string val1;
+    float val2 = 0.f;
+    double val3 = 0.;
+
+    unpackProperties(this->data, "Key-1", val1, "Key-2", val2, "Key-3", val3);
+
+    ASSERT_THAT(val1, Eq("string"));
+    ASSERT_THAT(val2, FloatEq(42.f));
+    ASSERT_THAT(val3, DoubleEq(15.));
+}
+
+TYPED_TEST(UnpackPropertiesTest,
+           unpackChangesOriginalDataWhenPassedAsNonConstReference)
+{
+    using namespace testing;
+
+    std::string val1, val2;
+
+    unpackProperties(this->data, "Key-1", val1);
+    unpackProperties(this->data, "Key-1", val2);
+
+    ASSERT_THAT(val1, Eq("string"));
+    ASSERT_THAT(val2, Not(Eq("string")));
+}
+
+TYPED_TEST(UnpackPropertiesTest,
+           unpackDoesntChangeOriginalDataWhenPassesAsConstReference)
+{
+    using namespace testing;
+
+    std::string val1, val2;
+
+    unpackProperties(Const(this->data), "Key-1", val1);
+    unpackProperties(Const(this->data), "Key-1", val2);
+
+    ASSERT_THAT(val1, Eq("string"));
+    ASSERT_THAT(val2, Eq("string"));
+}
+
+TYPED_TEST(UnpackPropertiesTest, throwsErrorWhenKeyIsMissing)
+{
+    using namespace testing;
+
+    std::string val1;
+    float val2 = 0.f;
+    double val3 = 0.;
+
+    auto error = captureException<exception::UnpackPropertyError>([&] {
+        unpackProperties(this->data, "Key-1", val1, "Key-4", val2, "Key-3",
+                         val3);
+    });
+
+    ASSERT_TRUE(error);
+    ASSERT_THAT(error->reason,
+                Eq(exception::UnpackPropertyError::reasonMissingProperty));
+    ASSERT_THAT(error->propertyName, Eq("Key-4"));
+}
+
+TYPED_TEST(UnpackPropertiesTest, throwsErrorWhenTypeDoesntMatch)
+{
+    using namespace testing;
+
+    std::string val1;
+    std::string val2;
+    double val3 = 0.;
+
+    auto error = captureException<exception::UnpackPropertyError>([&] {
+        unpackProperties(this->data, "Key-1", val1, "Key-2", val2, "Key-3",
+                         val3);
+    });
+
+    ASSERT_TRUE(error);
+    ASSERT_THAT(error->reason,
+                Eq(exception::UnpackPropertyError::reasonTypeNotMatched));
+    ASSERT_THAT(error->propertyName, Eq("Key-2"));
+}
+
+TYPED_TEST(UnpackPropertiesTest,
+           returnsUndefinedValueForDuplicatedKeysWhenDataIsNonConstReference)
+{
+    using namespace testing;
+    using namespace std::string_literals;
+
+    std::string val1;
+    float val2 = 0.f;
+    double val3 = 0.;
+    std::string val4;
+
+    unpackProperties(this->data, "Key-1", val1, "Key-2", val2, "Key-3", val3,
+                     "Key-1", val4);
+
+    ASSERT_THAT(val1, Eq("string"));
+    ASSERT_THAT(val2, FloatEq(42.f));
+    ASSERT_THAT(val3, DoubleEq(15.));
+    ASSERT_THAT(val4, Not(Eq("string")));
+}
+
+TYPED_TEST(UnpackPropertiesTest,
+           returnsValueForDuplicatedKeysWhenDataIsConstReference)
+{
+    using namespace testing;
+    using namespace std::string_literals;
+
+    std::string val1;
+    float val2 = 0.f;
+    double val3 = 0.;
+    std::string val4;
+
+    unpackProperties(Const(this->data), "Key-1", val1, "Key-2", val2, "Key-3",
+                     val3, "Key-1", val4);
+
+    ASSERT_THAT(val1, Eq("string"));
+    ASSERT_THAT(val2, FloatEq(42.f));
+    ASSERT_THAT(val3, DoubleEq(15.));
+    ASSERT_THAT(val4, Eq("string"));
+}
+
+struct UnpackPropertiesTest_ForVector :
+    public UnpackPropertiesTest<
+        std::vector<std::pair<std::string, VariantType>>>
+{};
+
+TEST_F(UnpackPropertiesTest_ForVector, silentlyDiscardsDuplicatedKeyInData)
+{
+    using namespace testing;
+    using namespace std::string_literals;
+
+    std::string val1;
+    float val2 = 0.f;
+    double val3 = 0.;
+
+    this->data.insert(this->data.end(),
+                      std::make_pair("Key-1"s, VariantType("string2"s)));
+
+    unpackProperties(this->data, "Key-1", val1, "Key-2", val2, "Key-3", val3);
+
+    ASSERT_THAT(val1, Eq("string"));
+    ASSERT_THAT(val2, FloatEq(42.f));
+    ASSERT_THAT(val3, DoubleEq(15.));
+}
+
+} // namespace sdbusplus
diff --git a/test/utility/type_traits.cpp b/test/utility/type_traits.cpp
index 58ade36..33bf8c1 100644
--- a/test/utility/type_traits.cpp
+++ b/test/utility/type_traits.cpp
@@ -2,7 +2,7 @@
 
 #include <type_traits>
 
-#include <gtest/gtest.h>
+#include <gmock/gmock.h>
 
 namespace
 {
@@ -26,4 +26,60 @@
                   "array_to_ptr_t<int, char[100]> != char[100]");
 }
 
+TEST(TypeTraits, HasMemberFind)
+{
+    using sdbusplus::utility::has_member_find_v;
+    using namespace testing;
+
+    ASSERT_THAT((has_member_find_v<std::map<std::string, int>>), Eq(true));
+    ASSERT_THAT((has_member_find_v<std::vector<std::pair<std::string, int>>>),
+                Eq(false));
+
+    struct Foo
+    {
+        using value_type = std::pair<int, int>;
+
+        void find(std::tuple_element_t<0, value_type>)
+        {}
+    };
+
+    struct Bar
+    {};
+
+    ASSERT_THAT(has_member_find_v<Foo>, Eq(true));
+    ASSERT_THAT(has_member_find_v<Foo&>, Eq(true));
+    ASSERT_THAT(has_member_find_v<const Foo&>, Eq(true));
+
+    ASSERT_THAT(has_member_find_v<Bar>, Eq(false));
+}
+
+TEST(TypeTraits, HasMemberContains)
+{
+    using sdbusplus::utility::has_member_contains_v;
+    using namespace testing;
+
+    // std::map has member_contains from c++20
+    ASSERT_THAT((has_member_contains_v<std::map<std::string, int>>), Eq(false));
+    ASSERT_THAT(
+        (has_member_contains_v<std::vector<std::pair<std::string, int>>>),
+        Eq(false));
+
+    struct Foo
+    {
+        using value_type = std::pair<int, int>;
+
+        void contains(std::tuple_element_t<0, value_type>)
+        {}
+    };
+
+    struct Bar
+    {};
+
+    ASSERT_THAT(has_member_contains_v<Foo>, Eq(true));
+    ASSERT_THAT(has_member_contains_v<Foo&>, Eq(true));
+    ASSERT_THAT(has_member_contains_v<const Foo&>, Eq(true));
+
+    ASSERT_THAT(has_member_contains_v<Bar>, Eq(false));
+}
+
 } // namespace