control: Add Modifier utility

Create a Modifier utility class that provides a doOp() function that can
modify a value based on the JSON config passed to its constructor.

With the JSON:
 {
   "operator": "minus",
   "value": 3
 }

 doOp() will subtract 3 from the passed in value.

 The only currently supported expression is 'minus'.

 This will be used by future actions to read a D-Bus property, subtract
 some configurable value from it, and store it in the Manager's
 parameter store.

Signed-off-by: Matt Spinler <spinler@us.ibm.com>
Change-Id: I31d9800f83e52a927a2061a9a896da9c19d35809
diff --git a/control/Makefile.am b/control/Makefile.am
index aca9c5c..d78ec47 100644
--- a/control/Makefile.am
+++ b/control/Makefile.am
@@ -70,7 +70,8 @@
 	json/actions/net_target_increase.cpp \
 	json/actions/net_target_decrease.cpp \
 	json/actions/timer_based_actions.cpp \
-	json/actions/mapped_floor.cpp
+	json/actions/mapped_floor.cpp \
+	json/utils/modifier.cpp
 else
 phosphor_fan_control_SOURCES += \
 	argument.cpp \
diff --git a/control/json/utils/modifier.cpp b/control/json/utils/modifier.cpp
new file mode 100644
index 0000000..6d00bc1
--- /dev/null
+++ b/control/json/utils/modifier.cpp
@@ -0,0 +1,173 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "modifier.hpp"
+
+#include "json/manager.hpp"
+
+#include <fmt/format.h>
+
+#include <phosphor-logging/log.hpp>
+
+using namespace phosphor::logging;
+
+namespace phosphor::fan::control::json
+{
+
+/**
+ * @brief Variant visitor to return a value of the template type specified.
+ */
+template <typename T>
+struct ToTypeVisitor
+{
+    template <typename U>
+    T operator()(const U& t) const
+    {
+        if constexpr (std::is_arithmetic_v<U> && std::is_arithmetic_v<T>)
+        {
+            return static_cast<T>(t);
+        }
+        throw std::invalid_argument(
+            "Non arithmetic type used in ToTypeVisitor");
+    }
+};
+
+/**
+ * @brief Implements the minus operator to subtract two values.
+ *
+ * With strings values, A - B removes all occurrences of B in A.
+ * Throws if the type is a bool.
+ */
+struct MinusOperator : public Modifier::BaseOperator
+{
+    PropertyVariantType operator()(double val) override
+    {
+        return val - std::visit(ToTypeVisitor<double>(), arg);
+    }
+
+    PropertyVariantType operator()(int32_t val) override
+    {
+        return val - std::visit(ToTypeVisitor<int32_t>(), arg);
+    }
+
+    PropertyVariantType operator()(int64_t val) override
+    {
+        return val - std::visit(ToTypeVisitor<int64_t>(), arg);
+    }
+
+    PropertyVariantType operator()(const std::string& val) override
+    {
+        // Remove all occurrences of arg from val.
+        auto value = val;
+        auto toRemove = std::get<std::string>(arg);
+        size_t pos;
+        while ((pos = value.find(toRemove)) != std::string::npos)
+        {
+            value.erase(pos, toRemove.size());
+        }
+
+        return value;
+    }
+
+    PropertyVariantType operator()(bool val) override
+    {
+        throw std::runtime_error{
+            "Bool not allowed as a 'minus' modifier value"};
+    }
+
+    MinusOperator(PropertyVariantType& arg) : arg(arg)
+    {}
+
+    PropertyVariantType arg;
+};
+
+Modifier::Modifier(const json& jsonObj)
+{
+    setValue(jsonObj);
+    setOperator(jsonObj);
+}
+
+void Modifier::setValue(const json& jsonObj)
+{
+    if (!jsonObj.contains("value"))
+    {
+        log<level::ERR>(
+            fmt::format("Modifier entry in JSON missing 'value': {}",
+                        jsonObj.dump())
+                .c_str());
+        throw std::invalid_argument("Invalid modifier JSON");
+    }
+
+    const auto& object = jsonObj.at("value");
+    if (auto boolPtr = object.get_ptr<const bool*>())
+    {
+        _value = *boolPtr;
+    }
+    else if (auto intPtr = object.get_ptr<const int64_t*>())
+    {
+        _value = *intPtr;
+    }
+    else if (auto doublePtr = object.get_ptr<const double*>())
+    {
+        _value = *doublePtr;
+    }
+    else if (auto stringPtr = object.get_ptr<const std::string*>())
+    {
+        _value = *stringPtr;
+    }
+    else
+    {
+        log<level::ERR>(
+            fmt::format(
+                "Invalid JSON type for value property in modifer json: {}",
+                jsonObj.dump())
+                .c_str());
+        throw std::invalid_argument("Invalid modifier JSON");
+    }
+}
+
+void Modifier::setOperator(const json& jsonObj)
+{
+    if (!jsonObj.contains("operator"))
+    {
+        log<level::ERR>(
+            fmt::format("Modifier entry in JSON missing 'operator': {}",
+                        jsonObj.dump())
+                .c_str());
+        throw std::invalid_argument("Invalid modifier JSON");
+    }
+
+    auto op = jsonObj["operator"].get<std::string>();
+
+    if (op == "minus")
+    {
+        _operator = std::make_unique<MinusOperator>(_value);
+    }
+    else
+    {
+        log<level::ERR>(fmt::format("Invalid operator in the modifier JSON: {}",
+                                    jsonObj.dump())
+                            .c_str());
+        throw std::invalid_argument("Invalid operator in the modifier JSON");
+    }
+}
+
+PropertyVariantType Modifier::doOp(const PropertyVariantType& val)
+{
+    return std::visit(*_operator, val);
+}
+
+} // namespace phosphor::fan::control::json
diff --git a/control/json/utils/modifier.hpp b/control/json/utils/modifier.hpp
new file mode 100644
index 0000000..3f59246
--- /dev/null
+++ b/control/json/utils/modifier.hpp
@@ -0,0 +1,120 @@
+/**
+ * Copyright © 2021 IBM Corporation
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+#pragma once
+
+#include "config_base.hpp"
+
+#include <string>
+#include <vector>
+
+namespace phosphor::fan::control::json
+{
+
+using json = nlohmann::json;
+
+/**
+ * @class Modifier
+ *
+ * This class provides a doOp() function to modify a PropertyVariantType value
+ * based on a JSON config passed into its constructor.
+ *
+ * For example, with the JSON:
+ * {
+ *   "operator": "minus",
+ *   "value": 3
+ * }
+ *
+ * When doOp() is called, it will subtract 3 from the value passed
+ * into doOp() and return the result.
+ *
+ * The valid operators are:
+ *  - "minus"
+ *
+ * To add a new operator, derive a new class from BaseOperator and
+ * then create it accordingly in setOperator.
+ */
+class Modifier
+{
+  public:
+    /**
+     * @brief Base class for operators
+     */
+    struct BaseOperator
+    {
+        virtual PropertyVariantType operator()(double val) = 0;
+
+        virtual PropertyVariantType operator()(int32_t val) = 0;
+
+        virtual PropertyVariantType operator()(int64_t val) = 0;
+
+        virtual PropertyVariantType operator()(const std::string& val) = 0;
+
+        virtual PropertyVariantType operator()(bool val) = 0;
+    };
+
+    Modifier() = delete;
+    ~Modifier() = default;
+    Modifier(const Modifier&) = delete;
+    Modifier& operator=(const Modifier&) = delete;
+    Modifier(Modifier&&) = delete;
+    Modifier& operator=(Modifier&&) = delete;
+
+    /**
+     * @brief Constructor
+     *
+     * @param[in] jsonObj - The JSON config object
+     */
+    Modifier(const json& jsonObj);
+
+    /**
+     * @brief Performs the operation
+     *
+     * @param[in] value - The variant to do the operation on
+     *
+     * @return PropertyVariantType - The result
+     */
+    PropertyVariantType doOp(const PropertyVariantType& value);
+
+  private:
+    /**
+     * @brief Parse and set the value
+     *
+     * @param[in] jsonObj - The JSON config object
+     */
+    void setValue(const json& jsonObj);
+
+    /**
+     * @brief Parse and set the operator
+     *
+     * @param[in] jsonObj - The JSON config object
+     */
+    void setOperator(const json& jsonObj);
+
+    /**
+     * @brief Subtracts _value from value
+     *
+     * @param[in] value - The value to subtract from
+     */
+    PropertyVariantType minus(const PropertyVariantType& value);
+
+    /** @brief The value used by the operator */
+    PropertyVariantType _value;
+
+    /** @brief The operator that will be used */
+    std::unique_ptr<BaseOperator> _operator;
+};
+
+} // namespace phosphor::fan::control::json