control: Complete profile configuration parsing

Profiles' have an active state that are determined by a given method
within the JSON configuration. Each method for determining a profile's
active state is mapped from its JSON configuration method name to the
handler function that is implemented to parse and return the profile's
active state based on the configuration. At this time, only an `all_of`
active state method is supported where all of the provided list of dbus
properties must match their given value for the profile to be active.

Tested:
    Profile method parsed and active state set appropriately

Change-Id: I258ecabccb849b2fa2662a157d6cb108af2c9886
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/control/json/config_base.hpp b/control/json/config_base.hpp
index 36a4b2c..3184cb5 100644
--- a/control/json/config_base.hpp
+++ b/control/json/config_base.hpp
@@ -15,6 +15,8 @@
  */
 #pragma once
 
+#include "types.hpp"
+
 #include <nlohmann/json.hpp>
 #include <phosphor-logging/log.hpp>
 
@@ -55,6 +57,47 @@
         return _name;
     }
 
+  protected:
+    /**
+     * @brief Determines the data type of a JSON configured parameter that is
+     * used as a variant within the fan control application and returns the
+     * value as that variant.
+     * @details Retrieves a JSON object by the first derived data type that
+     * is not null. Expected data types should appear in a logical order of
+     * conversion. i.e.) uint and int could both be uint
+     *
+     * @param[in] object - A single JSON object
+     *
+     * @return A `PropertyVariantType` variant containing the JSON object's
+     * value
+     */
+    static const PropertyVariantType getJsonValue(const json& object)
+    {
+        if (auto boolPtr = object.get_ptr<const bool*>())
+        {
+            return *boolPtr;
+        }
+        if (auto intPtr = object.get_ptr<const int64_t*>())
+        {
+            return *intPtr;
+        }
+        if (auto doublePtr = object.get_ptr<const double*>())
+        {
+            return *doublePtr;
+        }
+        if (auto stringPtr = object.get_ptr<const std::string*>())
+        {
+            return *stringPtr;
+        }
+
+        log<level::ERR>(
+            "Unsupported data type for JSON object's value",
+            entry("JSON_ENTRY=%s", object.dump().c_str()),
+            entry("SUPPORTED_TYPES=%s", "{bool, int, double, string}"));
+        throw std::runtime_error(
+            "Unsupported data type for JSON object's value");
+    }
+
   private:
     /* Name of the configuration object */
     std::string _name;
diff --git a/control/json/profile.cpp b/control/json/profile.cpp
index f7f80b1..696716e 100644
--- a/control/json/profile.cpp
+++ b/control/json/profile.cpp
@@ -15,18 +15,94 @@
  */
 #include "profile.hpp"
 
+#include "sdbusplus.hpp"
+
 #include <nlohmann/json.hpp>
 #include <phosphor-logging/log.hpp>
 #include <sdbusplus/bus.hpp>
 
+#include <algorithm>
+#include <iterator>
+#include <numeric>
+
 namespace phosphor::fan::control::json
 {
 
 using json = nlohmann::json;
 using namespace phosphor::logging;
 
+// String key must be in all lowercase for method lookup
+const std::map<std::string, methodHandler> Profile::_methods = {
+    {"all_of", Profile::allOf}};
+
 Profile::Profile(sdbusplus::bus::bus& bus, const json& jsonObj) :
-    ConfigBase(jsonObj), _bus(bus)
-{}
+    ConfigBase(jsonObj), _bus(bus), _active(false)
+{
+    setActive(jsonObj);
+}
+
+void Profile::setActive(const json& jsonObj)
+{
+    if (!jsonObj.contains("method") || !jsonObj["method"].contains("name"))
+    {
+        // Log error on missing profile method
+        log<level::ERR>("Missing required profile method",
+                        entry("JSON=%s", jsonObj.dump().c_str()));
+        throw std::runtime_error("Missing required profile method");
+    }
+    // The method to use in determining if the profile is active
+    auto method = jsonObj["method"]["name"].get<std::string>();
+    std::transform(method.begin(), method.end(), method.begin(), tolower);
+    auto handler = _methods.find(method);
+    if (handler != _methods.end())
+    {
+        // Call method for determining profile's active state
+        _active = handler->second(jsonObj["method"]);
+    }
+    else
+    {
+        // Construct list of available methods
+        auto methods = std::accumulate(
+            std::next(_methods.begin()), _methods.end(),
+            _methods.begin()->first, [](auto list, auto method) {
+                return std::move(list) + ", " + method.first;
+            });
+        log<level::ERR>("Configured method not available",
+                        entry("JSON=%s", jsonObj["method"].dump().c_str()),
+                        entry("METHODS_AVAILABLE=%s", methods.c_str()));
+    }
+}
+
+bool Profile::allOf(const json& method)
+{
+    if (!method.contains("properties"))
+    {
+        log<level::ERR>("Missing required all_of method properties list",
+                        entry("JSON=%s", method.dump().c_str()));
+        throw std::runtime_error(
+            "Missing required all_of method properties list");
+    }
+
+    return std::all_of(
+        method["properties"].begin(), method["properties"].end(),
+        [](const json& obj) {
+            if (!obj.contains("path") || !obj.contains("interface") ||
+                !obj.contains("property") || !obj.contains("value"))
+            {
+                log<level::ERR>(
+                    "Missing required all_of method property parameters",
+                    entry("JSON=%s", obj.dump().c_str()));
+                throw std::runtime_error(
+                    "Missing required all_of method parameters");
+            }
+            auto variant =
+                util::SDBusPlus::getPropertyVariant<PropertyVariantType>(
+                    obj["path"].get<std::string>(),
+                    obj["interface"].get<std::string>(),
+                    obj["property"].get<std::string>());
+
+            return getJsonValue(obj["value"]) == variant;
+        });
+}
 
 } // namespace phosphor::fan::control::json
diff --git a/control/json/profile.hpp b/control/json/profile.hpp
index ed80226..eacb1dc 100644
--- a/control/json/profile.hpp
+++ b/control/json/profile.hpp
@@ -24,6 +24,7 @@
 {
 
 using json = nlohmann::json;
+using methodHandler = std::function<bool(const json&)>;
 
 /**
  * @class Profile - Represents a configured fan control profile
@@ -58,9 +59,56 @@
      */
     Profile(sdbusplus::bus::bus& bus, const json& jsonObj);
 
+    /**
+     * @brief Get the active state
+     *
+     * @return The active state of the profile
+     */
+    inline bool isActive() const
+    {
+        return _active;
+    }
+
   private:
     /* The sdbusplus bus object */
     sdbusplus::bus::bus& _bus;
+
+    /* Active state of the profile */
+    bool _active;
+
+    /* Supported methods to their corresponding handler functions */
+    static const std::map<std::string, methodHandler> _methods;
+
+    /**
+     * @brief Parse and set the profile's active state
+     *
+     * @param[in] jsonObj - JSON object for the profile
+     *
+     * Sets the active state of the profile using the configured method of
+     * determining its active state.
+     */
+    void setActive(const json& jsonObj);
+
+    /**
+     * @brief An active state method where all must be true
+     *
+     * @param[in] method - JSON for the profile's method
+     *
+     * Active state method that takes a list of configured dbus properties where
+     * all of those properties must equal their configured values to set the
+     * profile to be active.
+     *
+     * "name": "all_of",
+     * "properties": [
+     *     {
+     *         "path": "[DBUS PATH]",
+     *         "interface": "[DBUS INTERFACE]",
+     *         "property": "[DBUS PROPERTY]",
+     *         "value": [VALUE TO BE ACTIVE]
+     *     }
+     * ]
+     */
+    static bool allOf(const json& method);
 };
 
 } // namespace phosphor::fan::control::json