regulators: Implements support for i2c_write_bit

Implements support for parsing the i2c_write_bit JSON elements in the
configuration file parser.

Signed-off-by: Bob King <Bob_King@wistron.com>
Change-Id: I52a16b3391872f2d451ac2bde02f0cb335f4dedd
diff --git a/phosphor-regulators/src/config_file_parser.cpp b/phosphor-regulators/src/config_file_parser.cpp
index 9ac6060..cdf758e 100644
--- a/phosphor-regulators/src/config_file_parser.cpp
+++ b/phosphor-regulators/src/config_file_parser.cpp
@@ -17,6 +17,7 @@
 #include "config_file_parser.hpp"
 
 #include "config_file_parser_error.hpp"
+#include "i2c_interface.hpp"
 #include "pmbus_utils.hpp"
 
 #include <exception>
@@ -102,9 +103,8 @@
     }
     else if (element.contains("i2c_write_bit"))
     {
-        // TODO: Not implemented yet
-        // action = parseI2CWriteBit(element["i2c_write_bit"]);
-        // ++propertyCount;
+        action = parseI2CWriteBit(element["i2c_write_bit"]);
+        ++propertyCount;
     }
     else if (element.contains("i2c_write_byte"))
     {
@@ -194,6 +194,32 @@
     return chassis;
 }
 
+std::unique_ptr<I2CWriteBitAction> parseI2CWriteBit(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Required register property
+    const json& regElement = getRequiredProperty(element, "register");
+    uint8_t reg = parseStringToUint8(regElement);
+    ++propertyCount;
+
+    // Required position property
+    const json& positionElement = getRequiredProperty(element, "position");
+    uint8_t position = parseBitPosition(positionElement);
+    ++propertyCount;
+
+    // Required value property
+    const json& valueElement = getRequiredProperty(element, "value");
+    uint8_t value = parseBitValue(valueElement);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_unique<I2CWriteBitAction>(reg, position, value);
+}
+
 std::unique_ptr<PMBusWriteVoutCommandAction>
     parsePMBusWriteVoutCommand(const json& element)
 {
diff --git a/phosphor-regulators/src/config_file_parser.hpp b/phosphor-regulators/src/config_file_parser.hpp
index 2b5ca0d..f96e6b4 100644
--- a/phosphor-regulators/src/config_file_parser.hpp
+++ b/phosphor-regulators/src/config_file_parser.hpp
@@ -17,6 +17,7 @@
 
 #include "action.hpp"
 #include "chassis.hpp"
+#include "i2c_write_bit_action.hpp"
 #include "pmbus_write_vout_command_action.hpp"
 #include "rule.hpp"
 
@@ -98,6 +99,56 @@
     parseActionArray(const nlohmann::json& element);
 
 /**
+ * Parses a JSON element containing a bit position (from 0-7).
+ *
+ * Returns the corresponding C++ uint8_t value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return uint8_t value
+ */
+inline uint8_t parseBitPosition(const nlohmann::json& element)
+{
+    // Verify element contains an integer
+    if (!element.is_number_integer())
+    {
+        throw std::invalid_argument{"Element is not an integer"};
+    }
+    int value = element;
+    if ((value < 0) || (value > 7))
+    {
+        throw std::invalid_argument{"Element is not a bit position"};
+    }
+    return static_cast<uint8_t>(value);
+}
+
+/**
+ * Parses a JSON element containing a bit value (0 or 1).
+ *
+ * Returns the corresponding C++ uint8_t value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return uint8_t value
+ */
+inline uint8_t parseBitValue(const nlohmann::json& element)
+{
+    // Verify element contains an integer
+    if (!element.is_number_integer())
+    {
+        throw std::invalid_argument{"Element is not an integer"};
+    }
+    int value = element;
+    if ((value < 0) || (value > 1))
+    {
+        throw std::invalid_argument{"Element is not a bit value"};
+    }
+    return static_cast<uint8_t>(value);
+}
+
+/**
  * Parses a JSON element containing a boolean.
  *
  * Returns the corresponding C++ boolean value.
@@ -151,6 +202,19 @@
 }
 
 /**
+ * Parses a JSON element containing an i2c_write_bit action.
+ *
+ * Returns the corresponding C++ I2CWriteBitAction object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return I2CWriteBitAction object
+ */
+std::unique_ptr<I2CWriteBitAction>
+    parseI2CWriteBit(const nlohmann::json& element);
+
+/**
  * Parses a JSON element containing an 8-bit signed integer.
  *
  * Returns the corresponding C++ int8_t value.
@@ -254,6 +318,56 @@
 }
 
 /**
+ * Parses a JSON element containing a hexadecimal string.
+ *
+ * Returns the corresponding C++ uint8_t value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return uint8_t value
+ */
+inline uint8_t parseStringToUint8(const nlohmann::json& element)
+{
+    std::string value = parseString(element);
+
+    bool isHex = (value.compare(0, 2, "0x") == 0) && (value.size() > 2) &&
+                 (value.size() < 5) &&
+                 (value.find_first_not_of("0123456789abcdefABCDEF", 2) ==
+                  std::string::npos);
+    if (!isHex)
+    {
+        throw std::invalid_argument{"Element is not hexadecimal string"};
+    }
+    return static_cast<uint8_t>(std::stoul(value, 0, 0));
+}
+
+/**
+ * Parses a JSON element containing an 8-bit unsigned integer.
+ *
+ * Returns the corresponding C++ uint8_t value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return uint8_t value
+ */
+inline uint8_t parseUint8(const nlohmann::json& element)
+{
+    // Verify element contains an integer
+    if (!element.is_number_integer())
+    {
+        throw std::invalid_argument{"Element is not an integer"};
+    }
+    int value = element;
+    if ((value < 0) || (value > UINT8_MAX))
+    {
+        throw std::invalid_argument{"Element is not an 8-bit unsigned integer"};
+    }
+    return static_cast<uint8_t>(value);
+}
+
+/**
  * Verifies that the specified JSON element is a JSON array.
  *
  * Throws an invalid_argument exception if the element is not an array.
diff --git a/phosphor-regulators/test/config_file_parser_tests.cpp b/phosphor-regulators/test/config_file_parser_tests.cpp
index 83d5699..c26f472 100644
--- a/phosphor-regulators/test/config_file_parser_tests.cpp
+++ b/phosphor-regulators/test/config_file_parser_tests.cpp
@@ -17,6 +17,8 @@
 #include "chassis.hpp"
 #include "config_file_parser.hpp"
 #include "config_file_parser_error.hpp"
+#include "i2c_interface.hpp"
+#include "i2c_write_bit_action.hpp"
 #include "pmbus_utils.hpp"
 #include "pmbus_write_vout_command_action.hpp"
 #include "rule.hpp"
@@ -246,7 +248,19 @@
     // TODO: Not implemented yet
 
     // Test where works: i2c_write_bit action type specified
-    // TODO: Not implemented yet
+    {
+        const json element = R"(
+            {
+              "i2c_write_bit": {
+                "register": "0xA0",
+                "position": 3,
+                "value": 0
+              }
+            }
+        )"_json;
+        std::unique_ptr<Action> action = parseAction(element);
+        EXPECT_NE(action.get(), nullptr);
+    }
 
     // Test where works: i2c_write_byte action type specified
     // TODO: Not implemented yet
@@ -368,6 +382,112 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParseBitPosition)
+{
+    // Test where works: 0
+    {
+        const json element = R"( 0 )"_json;
+        uint8_t value = parseBitPosition(element);
+        EXPECT_EQ(value, 0);
+    }
+
+    // Test where works: 7
+    {
+        const json element = R"( 7 )"_json;
+        uint8_t value = parseBitPosition(element);
+        EXPECT_EQ(value, 7);
+    }
+
+    // Test where fails: Element is not an integer
+    try
+    {
+        const json element = R"( 1.03 )"_json;
+        parseBitPosition(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an integer");
+    }
+
+    // Test where fails: Value < 0
+    try
+    {
+        const json element = R"( -1 )"_json;
+        parseBitPosition(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a bit position");
+    }
+
+    // Test where fails: Value > 7
+    try
+    {
+        const json element = R"( 8 )"_json;
+        parseBitPosition(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a bit position");
+    }
+}
+
+TEST(ConfigFileParserTests, ParseBitValue)
+{
+    // Test where works: 0
+    {
+        const json element = R"( 0 )"_json;
+        uint8_t value = parseBitValue(element);
+        EXPECT_EQ(value, 0);
+    }
+
+    // Test where works: 1
+    {
+        const json element = R"( 1 )"_json;
+        uint8_t value = parseBitValue(element);
+        EXPECT_EQ(value, 1);
+    }
+
+    // Test where fails: Element is not an integer
+    try
+    {
+        const json element = R"( 0.5 )"_json;
+        parseBitValue(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an integer");
+    }
+
+    // Test where fails: Value < 0
+    try
+    {
+        const json element = R"( -1 )"_json;
+        parseBitValue(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a bit value");
+    }
+
+    // Test where fails: Value > 1
+    try
+    {
+        const json element = R"( 2 )"_json;
+        parseBitValue(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a bit value");
+    }
+}
+
 TEST(ConfigFileParserTests, ParseBoolean)
 {
     // Test where works: true
@@ -484,6 +604,160 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParseI2CWriteBit)
+{
+    // Test where works
+    {
+        const json element = R"(
+            {
+              "register": "0xA0",
+              "position": 3,
+              "value": 0
+            }
+        )"_json;
+        std::unique_ptr<I2CWriteBitAction> action = parseI2CWriteBit(element);
+        EXPECT_EQ(action->getRegister(), 0xA0);
+        EXPECT_EQ(action->getPosition(), 3);
+        EXPECT_EQ(action->getValue(), 0);
+    }
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "register": "0xA0",
+              "position": 3,
+              "value": 0,
+              "foo": 3
+            }
+        )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element contains an invalid property");
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "0xFF", "0x01" ] )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an object");
+    }
+
+    // Test where fails: register value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "register": "0xAG",
+              "position": 3,
+              "value": 0
+            }
+        )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+
+    // Test where fails: position value is invalid
+    try
+    {
+        const json element = R"(
+                {
+                  "register": "0xA0",
+                  "position": 8,
+                  "value": 0
+                }
+            )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a bit position");
+    }
+
+    // Test where fails: value value is invalid
+    try
+    {
+        const json element = R"(
+                {
+                  "register": "0xA0",
+                  "position": 3,
+                  "value": 2
+                }
+            )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a bit value");
+    }
+
+    // Test where fails: Required register property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "position": 3,
+              "value": 0
+            }
+        )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: register");
+    }
+
+    // Test where fails: Required position property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "register": "0xA0",
+              "value": 0
+            }
+        )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: position");
+    }
+
+    // Test where fails: Required value property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "register": "0xA0",
+              "position": 3
+            }
+        )"_json;
+        parseI2CWriteBit(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: value");
+    }
+}
+
 TEST(ConfigFileParserTests, ParsePMBusWriteVoutCommand)
 {
     // Test where works: Only required properties specified
@@ -959,6 +1233,167 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParseStringToUint8)
+{
+    // Test where works: "0xFF"
+    {
+        const json element = R"( "0xFF" )"_json;
+        uint8_t value = parseStringToUint8(element);
+        EXPECT_EQ(value, 0xFF);
+    }
+
+    // Test where works: "0xff"
+    {
+        const json element = R"( "0xff" )"_json;
+        uint8_t value = parseStringToUint8(element);
+        EXPECT_EQ(value, 0xff);
+    }
+
+    // Test where works: "0xf"
+    {
+        const json element = R"( "0xf" )"_json;
+        uint8_t value = parseStringToUint8(element);
+        EXPECT_EQ(value, 0xf);
+    }
+
+    // Test where fails: "0xfff"
+    try
+    {
+        const json element = R"( "0xfff" )"_json;
+        parseStringToUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+
+    // Test where fails: "0xAG"
+    try
+    {
+        const json element = R"( "0xAG" )"_json;
+        parseStringToUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+
+    // Test where fails: "ff"
+    try
+    {
+        const json element = R"( "ff" )"_json;
+        parseStringToUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+
+    // Test where fails: ""
+    try
+    {
+        const json element = "";
+        parseStringToUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element contains an empty string");
+    }
+
+    // Test where fails: "f"
+    try
+    {
+        const json element = R"( "f" )"_json;
+        parseStringToUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+
+    // Test where fails: "0x"
+    try
+    {
+        const json element = R"( "0x" )"_json;
+        parseStringToUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+
+    // Test where fails: "0Xff"
+    try
+    {
+        const json element = R"( "0XFF" )"_json;
+        parseStringToUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
+}
+
+TEST(ConfigFileParserTests, ParseUint8)
+{
+    // Test where works: 0
+    {
+        const json element = R"( 0 )"_json;
+        uint8_t value = parseUint8(element);
+        EXPECT_EQ(value, 0);
+    }
+
+    // Test where works: UINT8_MAX
+    {
+        const json element = R"( 255 )"_json;
+        uint8_t value = parseUint8(element);
+        EXPECT_EQ(value, 255);
+    }
+
+    // Test where fails: Element is not an integer
+    try
+    {
+        const json element = R"( 1.03 )"_json;
+        parseUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an integer");
+    }
+
+    // Test where fails: Value < 0
+    try
+    {
+        const json element = R"( -1 )"_json;
+        parseUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
+    }
+
+    // Test where fails: Value > UINT8_MAX
+    try
+    {
+        const json element = R"( 256 )"_json;
+        parseUint8(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
+    }
+}
+
 TEST(ConfigFileParserTests, VerifyIsArray)
 {
     // Test where element is an array