control: Set groups & zones on event actions

Configure and set the groups to be used per action of an event along
with setting the list of zones the action should be run against. Groups
configured on an action are appended to the event's configured groups.
This is intended to allow a shared set of groups across all actions of
an event and then include additional groups specific to the action.
Also, zones configured on an action result in only those zones being
used by the action whereas when no zones are configured on the action,
all of the available zones to the event are used.

Change-Id: I5986398eec4c6cf5ac88f5725f2b877bd2551fc7
Signed-off-by: Matthew Barth <msbarth@us.ibm.com>
diff --git a/control/json/actions/action.hpp b/control/json/actions/action.hpp
index 06759cc..05fc874 100644
--- a/control/json/actions/action.hpp
+++ b/control/json/actions/action.hpp
@@ -183,8 +183,10 @@
      *
      * @return Pointer to the action object.
      */
-    static std::unique_ptr<ActionBase> getAction(const std::string& name,
-                                                 const json& jsonObj)
+    static std::unique_ptr<ActionBase>
+        getAction(const std::string& name, const json& jsonObj,
+                  const std::vector<Group>& groups,
+                  std::vector<std::reference_wrapper<Zone>>&& zones)
     {
         auto it = actions.find(name);
         if (it != actions.end())
diff --git a/control/json/event.cpp b/control/json/event.cpp
index f02a590..3db50aa 100644
--- a/control/json/event.cpp
+++ b/control/json/event.cpp
@@ -62,6 +62,41 @@
     }
 }
 
+void Event::configGroup(Group& group, const json& jsonObj)
+{
+    if (!jsonObj.contains("interface") || !jsonObj.contains("property") ||
+        !jsonObj["property"].contains("name"))
+    {
+        log<level::ERR>("Missing required group attribute",
+                        entry("JSON=%s", jsonObj.dump().c_str()));
+        throw std::runtime_error("Missing required group attribute");
+    }
+
+    // Get the group members' interface
+    auto intf = jsonObj["interface"].get<std::string>();
+    group.setInterface(intf);
+
+    // Get the group members' property name
+    auto prop = jsonObj["property"]["name"].get<std::string>();
+    group.setProperty(prop);
+
+    // Get the group members' data type
+    if (jsonObj["property"].contains("type"))
+    {
+        std::optional<std::string> type =
+            jsonObj["property"]["type"].get<std::string>();
+        group.setType(type);
+    }
+
+    // Get the group members' expected value
+    if (jsonObj["property"].contains("value"))
+    {
+        std::optional<PropertyVariantType> value =
+            getJsonValue(jsonObj["property"]["value"]);
+        group.setValue(value);
+    }
+}
+
 void Event::setPrecond(const json& jsonObj,
                        std::map<configKey, std::unique_ptr<Group>>& groups)
 {
@@ -81,80 +116,134 @@
 void Event::setGroups(const json& jsonObj,
                       std::map<configKey, std::unique_ptr<Group>>& groups)
 {
-    for (const auto& group : jsonObj["groups"])
+    for (const auto& jsonGrp : jsonObj["groups"])
     {
-        if (!group.contains("name") || !group.contains("interface") ||
-            !group.contains("property") || !group["property"].contains("name"))
+        if (!jsonGrp.contains("name"))
         {
-            log<level::ERR>("Missing required event group attributes",
-                            entry("JSON=%s", group.dump().c_str()));
-            throw std::runtime_error("Missing required event group attributes");
-        }
-
-        // Get the group members' interface
-        auto intf = group["interface"].get<std::string>();
-
-        // Get the group members' property name
-        auto prop = group["property"]["name"].get<std::string>();
-
-        // Get the group members' data type
-        std::optional<std::string> type = std::nullopt;
-        if (group["property"].contains("type"))
-        {
-            type = group["property"]["type"].get<std::string>();
-        }
-
-        // Get the group members' expected value
-        std::optional<PropertyVariantType> value = std::nullopt;
-        if (group["property"].contains("value"))
-        {
-            value = getJsonValue(group["property"]["value"]);
+            log<level::ERR>("Missing required group name attribute",
+                            entry("JSON=%s", jsonGrp.dump().c_str()));
+            throw std::runtime_error("Missing required group name attribute");
         }
 
         configKey eventProfile =
-            std::make_pair(group["name"].get<std::string>(), _profiles);
+            std::make_pair(jsonGrp["name"].get<std::string>(), _profiles);
         auto grpEntry = std::find_if(
             groups.begin(), groups.end(), [&eventProfile](const auto& grp) {
                 return Manager::inConfig(grp.first, eventProfile);
             });
         if (grpEntry != groups.end())
         {
-            auto grp = Group(*grpEntry->second);
-            grp.setInterface(intf);
-            grp.setProperty(prop);
-            grp.setType(type);
-            grp.setValue(value);
-            _groups.emplace_back(grp);
+            auto group = Group(*grpEntry->second);
+            configGroup(group, jsonGrp);
+            _groups.emplace_back(group);
         }
     }
-
-    if (_groups.empty())
-    {
-        auto msg = fmt::format(
-            "No groups configured for event {} in its active profile(s)",
-            getName());
-        log<level::ERR>(msg.c_str());
-        throw std::runtime_error(msg);
-    }
 }
 
 void Event::setActions(const json& jsonObj,
                        std::map<configKey, std::unique_ptr<Group>>& groups)
 {
-    for (const auto& action : jsonObj["actions"])
+    for (const auto& jsonAct : jsonObj["actions"])
     {
-        if (!action.contains("name"))
+        if (!jsonAct.contains("name"))
         {
             log<level::ERR>("Missing required event action name",
-                            entry("JSON=%s", action.dump().c_str()));
+                            entry("JSON=%s", jsonAct.dump().c_str()));
             throw std::runtime_error("Missing required event action name");
         }
-        // TODO Append action specific groups to event groups list separate from
-        // each action in the event and pass reference of group to action
-        // TODO Consider supporting zones per action and pass a reference to the
-        // zone(s) the action should be run against
-        auto actObj =
-            ActionFactory::getAction(action["name"].get<std::string>(), action);
+
+        // Append action specific groups to the list of event groups for each
+        // action in the event
+        auto actionGroups = _groups;
+        if (jsonObj.contains("groups"))
+        {
+            for (const auto& jsonGrp : jsonObj["groups"])
+            {
+                if (!jsonGrp.contains("name"))
+                {
+                    log<level::ERR>("Missing required group name attribute",
+                                    entry("JSON=%s", jsonGrp.dump().c_str()));
+                    throw std::runtime_error(
+                        "Missing required group name attribute");
+                }
+
+                configKey eventProfile = std::make_pair(
+                    jsonGrp["name"].get<std::string>(), _profiles);
+                auto grpEntry = std::find_if(groups.begin(), groups.end(),
+                                             [&eventProfile](const auto& grp) {
+                                                 return Manager::inConfig(
+                                                     grp.first, eventProfile);
+                                             });
+                if (grpEntry != groups.end())
+                {
+                    auto group = Group(*grpEntry->second);
+                    configGroup(group, jsonGrp);
+                    actionGroups.emplace_back(group);
+                }
+            }
+        }
+        if (actionGroups.empty())
+        {
+            log<level::DEBUG>(
+                fmt::format("No groups configured for event {}'s action {} "
+                            "based on the active profile(s)",
+                            getName(), jsonAct["name"].get<std::string>())
+                    .c_str());
+        }
+
+        // Determine list of zones action should be run against
+        std::vector<std::reference_wrapper<Zone>> actionZones;
+        if (!jsonObj.contains("zones"))
+        {
+            // No zones configured on the action results in the action running
+            // against all zones matching the event's active profiles
+            for (const auto& zone : _zones)
+            {
+                configKey eventProfile =
+                    std::make_pair(zone.second->getName(), _profiles);
+                auto zoneEntry = std::find_if(_zones.begin(), _zones.end(),
+                                              [&eventProfile](const auto& z) {
+                                                  return Manager::inConfig(
+                                                      z.first, eventProfile);
+                                              });
+                if (zoneEntry != _zones.end())
+                {
+                    actionZones.emplace_back(*zoneEntry->second);
+                }
+            }
+        }
+        else
+        {
+            // Zones configured on the action result in the action only running
+            // against those zones if they match the event's active profiles
+            for (const auto& jsonZone : jsonObj["zones"])
+            {
+                configKey eventProfile =
+                    std::make_pair(jsonZone.get<std::string>(), _profiles);
+                auto zoneEntry = std::find_if(_zones.begin(), _zones.end(),
+                                              [&eventProfile](const auto& z) {
+                                                  return Manager::inConfig(
+                                                      z.first, eventProfile);
+                                              });
+                if (zoneEntry != _zones.end())
+                {
+                    actionZones.emplace_back(*zoneEntry->second);
+                }
+            }
+        }
+        if (actionZones.empty())
+        {
+            log<level::DEBUG>(
+                fmt::format("No zones configured for event {}'s action {} "
+                            "based on the active profile(s)",
+                            getName(), jsonAct["name"].get<std::string>())
+                    .c_str());
+        }
+
+        // Create the action for the event
+        auto actObj = ActionFactory::getAction(
+            jsonAct["name"].get<std::string>(), jsonAct,
+            std::move(actionGroups), std::move(actionZones));
         if (actObj)
         {
             _actions.emplace_back(std::move(actObj));
diff --git a/control/json/event.hpp b/control/json/event.hpp
index 4c4d387..4b18e98 100644
--- a/control/json/event.hpp
+++ b/control/json/event.hpp
@@ -129,6 +129,16 @@
     std::vector<std::unique_ptr<ActionBase>> _actions;
 
     /**
+     * @brief Parse group parameters and configure a group object
+     *
+     * @param[in] group - Group object to get configured
+     * @param[in] jsonObj - JSON object for the group
+     *
+     * Configures a given group from a set of JSON configuration attributes
+     */
+    void configGroup(Group& group, const json& jsonObj);
+
+    /**
      * @brief Parse and set the event's precondition(OPTIONAL)
      *
      * @param[in] jsonObj - JSON object for the event