Add conditional callbacks

Enable conditional application of callbacks.

Change-Id: I9d765e5f585aac40994b65da3b51ea891beae9bf
Signed-off-by: Brad Bishop <bradleyb@fuzziesquirrel.com>
diff --git a/src/Makefile.am b/src/Makefile.am
index c77c0a8..4d6658d 100644
--- a/src/Makefile.am
+++ b/src/Makefile.am
@@ -23,6 +23,8 @@
 
 TEMPLATES = \
 	templates/callbackgroup.mako.cpp \
+	templates/conditional.mako.cpp \
+	templates/count.mako.cpp \
 	templates/generated.mako.hpp \
 	templates/journal.mako.cpp
 
diff --git a/src/callback.hpp b/src/callback.hpp
index 7c6f69b..f7886cb 100644
--- a/src/callback.hpp
+++ b/src/callback.hpp
@@ -28,6 +28,50 @@
         virtual void operator()() = 0;
 };
 
+/** @class Conditional
+ *  @brief Condition interface.
+ *
+ *  Conditions of any type can be tested for true or false.
+ */
+class Conditional
+{
+    public:
+        Conditional() = default;
+        Conditional(const Conditional&) = delete;
+        Conditional(Conditional&&) = default;
+        Conditional& operator=(const Conditional&) = delete;
+        Conditional& operator=(Conditional&&) = default;
+        virtual ~Conditional() = default;
+
+        /** @brief Test the condition. */
+        virtual bool operator()() = 0;
+};
+
+/** @class IndexedConditional
+ *  @brief Condition with an index.
+ */
+class IndexedConditional : public Conditional
+{
+    public:
+        IndexedConditional() = delete;
+        IndexedConditional(const IndexedConditional&) = delete;
+        IndexedConditional(IndexedConditional&&) = default;
+        IndexedConditional& operator=(const IndexedConditional&) = delete;
+        IndexedConditional& operator=(IndexedConditional&&) = default;
+        virtual ~IndexedConditional() = default;
+
+        explicit IndexedConditional(const PropertyIndex& conditionIndex)
+            : Conditional(), index(conditionIndex) {}
+
+        /** @brief Test the condition. */
+        virtual bool operator()() override = 0;
+
+    protected:
+
+        /** @brief Property names and their associated storage. */
+        const PropertyIndex& index;
+};
+
 /** @class IndexedCallback
  *  @brief Callback with an index.
  */
@@ -90,6 +134,41 @@
         const std::vector<size_t>& graph;
 };
 
+/** @class ConditionalCallback
+ *  @brief Callback adaptor that asssociates a condition with a callback.
+ */
+template <typename CallbackAccess>
+class ConditionalCallback: public Callback
+{
+    public:
+        ConditionalCallback() = delete;
+        ConditionalCallback(const ConditionalCallback&) = delete;
+        ConditionalCallback(ConditionalCallback&&) = default;
+        ConditionalCallback& operator=(const ConditionalCallback&) = delete;
+        ConditionalCallback& operator=(ConditionalCallback&&) = default;
+        ~ConditionalCallback() = default;
+        ConditionalCallback(
+            const std::vector<size_t>& graphEntry,
+            Conditional& cond)
+            : graph(graphEntry), condition(cond) {}
+
+        /** @brief Run the callback if the condition is satisfied. */
+        void operator()() override
+        {
+            if (condition())
+            {
+                (*CallbackAccess::get()[graph[0]])();
+            }
+        }
+
+    private:
+        /** @brief The index of the callback to conditionally invoke. */
+        const std::vector<size_t>& graph;
+
+        /** @brief The condition to test. */
+        Conditional& condition;
+};
+
 } // namespace monitoring
 } // namespace dbus
 } // namespace phosphor
diff --git a/src/count.hpp b/src/count.hpp
new file mode 100644
index 0000000..a588ccc
--- /dev/null
+++ b/src/count.hpp
@@ -0,0 +1,82 @@
+#pragma once
+
+#include "callback.hpp"
+#include "data_types.hpp"
+
+namespace phosphor
+{
+namespace dbus
+{
+namespace monitoring
+{
+
+/** @class CountCondition
+ *  @brief Count properties that satisfy a condition.
+ *
+ *  When invoked, a count class instance performs its condition
+ *  test in two passes.
+ *
+ *  In pass one, apply a C++ relational operator to the value of
+ *  each property in the index and a value provided by the
+ *  configuration file.
+ *
+ *  Count the number of properties that pass the test in pass
+ *  one.  In pass two, apply a second C++ relational operator
+ *  to the number of properties that pass the test from pass one
+ *  to a count provided by the configuration file.
+ */
+template <typename T>
+class CountCondition : public IndexedConditional
+{
+    public:
+        CountCondition() = delete;
+        CountCondition(const CountCondition&) = default;
+        CountCondition(CountCondition&&) = default;
+        CountCondition& operator=(const CountCondition&) = default;
+        CountCondition& operator=(CountCondition&&) = default;
+        ~CountCondition() = default;
+
+        CountCondition(
+            const PropertyIndex& conditionIndex,
+            const std::function<bool(size_t)>& _countOp,
+            const std::function<bool(T)>& _propertyOp) :
+            IndexedConditional(conditionIndex),
+            countOp(_countOp),
+            propertyOp(_propertyOp) {}
+
+        bool operator()() override
+        {
+            // Count the number of properties in the index that
+            // pass the condition specified in the config file.
+            auto count = std::count_if(
+                             index.cbegin(),
+                             index.cend(),
+                             [this](const auto & item)
+            // *INDENT-OFF*
+                             {
+                                 const auto& storage = std::get<2>(
+                                     item.second);
+                                 // Don't count properties that don't exist.
+                                 if (storage.get().empty())
+                                 {
+                                     return false;
+                                 }
+                                 const auto& value = any_ns::any_cast<T>(
+                                     storage);
+                                 return propertyOp(value);
+                             });
+            // *INDENT-ON*
+
+            // Now apply the count condition to the count.
+            return countOp(count);
+        }
+
+    private:
+        /** @brief The comparison to perform on the count. */
+        std::function<bool(size_t)> countOp;
+        /** @brief The comparison to perform on each property. */
+        std::function<bool(T)> propertyOp;
+};
+} // namespace monitoring
+} // namespace dbus
+} // namespace phosphor
diff --git a/src/example/example.yaml b/src/example/example.yaml
index 0f286dc..65f6537 100644
--- a/src/example/example.yaml
+++ b/src/example/example.yaml
@@ -93,3 +93,30 @@
   callback: group
   members:
     - example journal callback
+
+- name: example count condition
+  description: >
+    'Conditions or conditional callbacks apply a test prior to invoking
+    the callback function.
+
+    All conditional callbacks must specify the callback to issue if
+    the condition evaulates.
+
+    The count condition applies the op comparison operator to the value of each
+    property in the specified groups.  It then counts the number of properties
+    that pass the comparison, and applies another comparison on the result
+    against the specified bound.
+
+    For example, a callback that requires at least three temperature sensors
+    in the group to be higher than 115 degrees might use a count condition
+    with an op of >, a count op of >=, a bound of 115, and a countbound of 3.'
+
+  class: condition
+  condition: count
+  paths: example path group
+  properties: example property group
+  callback: example callback group
+  countop: '>='
+  countbound: 3
+  op: '>='
+  bound: 115
diff --git a/src/pdmgen.py b/src/pdmgen.py
index b20436e..4a958e1 100755
--- a/src/pdmgen.py
+++ b/src/pdmgen.py
@@ -420,6 +420,99 @@
         super(Callback, self).__init__(**kw)
 
 
+class ConditionCallback(ConfigEntry, Renderer):
+    '''Handle the journal callback config file directive.'''
+
+    def __init__(self, *a, **kw):
+        self.condition = kw.pop('condition')
+        self.instance = kw.pop('instance')
+        super(ConditionCallback, self).__init__(**kw)
+
+    def factory(self, objs):
+        '''Create a graph instance for this callback.'''
+
+        args = {
+            'configfile': self.configfile,
+            'members': [self.instance],
+            'class': 'callbackgroup',
+            'callbackgroup': 'callback',
+            'name': [self.instance]
+        }
+
+        entry = CallbackGraphEntry(**args)
+        add_unique(entry, objs, config=self.configfile)
+
+        super(ConditionCallback, self).factory(objs)
+
+    def setup(self, objs):
+        '''Resolve condition and graph entry.'''
+
+        self.graph = get_index(
+            objs,
+            'callbackgroup',
+            [self.instance],
+            config=self.configfile)
+
+        self.condition = get_index(
+            objs,
+            'condition',
+            self.name,
+            config=self.configfile)
+
+        super(ConditionCallback, self).setup(objs)
+
+    def construct(self, loader, indent):
+        return self.render(
+            loader,
+            'conditional.mako.cpp',
+            c=self,
+            indent=indent)
+
+
+class Condition(HasPropertyIndex):
+    '''Interface and common logic for conditions.'''
+
+    def __init__(self, *a, **kw):
+        self.callback = kw.pop('callback')
+        super(Condition, self).__init__(**kw)
+
+    def factory(self, objs):
+        '''Create a callback instance for this conditional.'''
+
+        args = {
+            'configfile': self.configfile,
+            'condition': self.name,
+            'class': 'callback',
+            'callback': 'conditional',
+            'instance': self.callback,
+            'name': self.name,
+        }
+
+        callback = ConditionCallback(**args)
+        add_unique(callback, objs, config=self.configfile)
+        callback.factory(objs)
+
+        super(Condition, self).factory(objs)
+
+
+class CountCondition(Condition, Renderer):
+    '''Handle the count condition config file directive.'''
+
+    def __init__(self, *a, **kw):
+        self.countop = kw.pop('countop')
+        self.countbound = kw.pop('countbound')
+        self.op = kw.pop('op')
+        self.bound = kw.pop('bound')
+        super(CountCondition, self).__init__(**kw)
+
+    def construct(self, loader, indent):
+        return self.render(
+            loader,
+            'count.mako.cpp',
+            c=self,
+            indent=indent)
+
+
 class Journal(Callback, Renderer):
     '''Handle the journal callback config file directive.'''
 
@@ -526,6 +619,9 @@
                 'journal': Journal,
                 'group': GroupOfCallbacks,
             },
+            'condition': {
+                'count': CountCondition,
+            },
         }
 
         if cls not in class_map:
@@ -617,6 +713,7 @@
         self.watches = kw.pop('watch', [])
         self.callbacks = kw.pop('callback', [])
         self.callbackgroups = kw.pop('callbackgroup', [])
+        self.conditions = kw.pop('condition', [])
 
         super(Everything, self).__init__(**kw)
 
@@ -640,6 +737,7 @@
                     instancegroups=self.instancegroups,
                     callbacks=self.callbacks,
                     callbackgroups=self.callbackgroups,
+                    conditions=self.conditions,
                     indent=Indent()))
 
 if __name__ == '__main__':
diff --git a/src/templates/conditional.mako.cpp b/src/templates/conditional.mako.cpp
new file mode 100644
index 0000000..3cc9564
--- /dev/null
+++ b/src/templates/conditional.mako.cpp
@@ -0,0 +1,3 @@
+std::make_unique<ConditionalCallback<ConfigPropertyCallbacks>>(
+${indent(1)}ConfigPropertyCallbackGroups::get()[${c.graph}],
+${indent(1)}*ConfigConditions::get()[${c.condition}])\
diff --git a/src/templates/count.mako.cpp b/src/templates/count.mako.cpp
new file mode 100644
index 0000000..d348518
--- /dev/null
+++ b/src/templates/count.mako.cpp
@@ -0,0 +1,4 @@
+std::make_unique<CountCondition<${c.datatype}>>(
+${indent(1)}ConfigPropertyIndicies::get()[${c.instances}],
+${indent(1)}[](const auto& item){return item ${c.countop} ${c.countbound};},
+${indent(1)}[](const auto& item){return item ${c.op} ${c.bound};})\
diff --git a/src/templates/generated.mako.hpp b/src/templates/generated.mako.hpp
index 67b9149..a4819e2 100644
--- a/src/templates/generated.mako.hpp
+++ b/src/templates/generated.mako.hpp
@@ -4,6 +4,7 @@
 
 #include <array>
 #include <string>
+#include "count.hpp"
 #include "data_types.hpp"
 #include "journal.hpp"
 #include "propertywatchimpl.hpp"
@@ -145,6 +146,22 @@
     }
 };
 
+struct ConfigConditions
+{
+    using Conditions = std::array<std::unique_ptr<Conditional>, ${len(conditions)}>;
+
+    static auto& get()
+    {
+        static const Conditions propertyConditions =
+        {
+% for c in conditions:
+            ${c.construct(loader, indent=indent +3)},
+% endfor
+        };
+        return propertyConditions;
+    }
+};
+
 struct ConfigPropertyCallbacks
 {
     using Callbacks = std::array<std::unique_ptr<Callback>, ${len(callbacks)}>;