json_parser_utils: Add variable support

Enhance the json_parser_utils functions to support optional usage of
variables in JSON values.

Variables are specified using the syntax `${variable_name}`.

Variable values are specified in an optional new parameter to the
parsing functions. Parsing functions will replace the variable with the
corresponding variable value.

Example:
```
  {
    "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}"
  }
```

Tested:
* Ran automated test cases.

Change-Id: Ib8f5d9b27ccc96ca9d16eb9a044321233f81ba18
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/json_parser_utils.cpp b/json_parser_utils.cpp
index be34323..25b2e9a 100644
--- a/json_parser_utils.cpp
+++ b/json_parser_utils.cpp
@@ -16,18 +16,217 @@
 
 #include "json_parser_utils.hpp"
 
+#include <charconv>
+#include <regex>
+
 namespace phosphor::power::json_parser_utils
 {
 
-std::vector<uint8_t> parseHexByteArray(const nlohmann::json& element)
+const std::map<std::string, std::string> NO_VARIABLES{};
+
+static std::regex VARIABLE_REGEX{R"(\$\{([A-Za-z0-9_]+)\})"};
+
+uint8_t parseBitPosition(const nlohmann::json& element,
+                         const std::map<std::string, std::string>& variables)
+{
+    int value = parseInteger(element, variables);
+    if ((value < 0) || (value > 7))
+    {
+        throw std::invalid_argument{"Element is not a bit position"};
+    }
+    return static_cast<uint8_t>(value);
+}
+
+uint8_t parseBitValue(const nlohmann::json& element,
+                      const std::map<std::string, std::string>& variables)
+{
+    int value = parseInteger(element, variables);
+    if ((value < 0) || (value > 1))
+    {
+        throw std::invalid_argument{"Element is not a bit value"};
+    }
+    return static_cast<uint8_t>(value);
+}
+
+bool parseBoolean(const nlohmann::json& element,
+                  const std::map<std::string, std::string>& variables)
+{
+    if (element.is_boolean())
+    {
+        return element.get<bool>();
+    }
+
+    if (element.is_string() && !variables.empty())
+    {
+        std::string value = parseString(element, true, variables);
+        if (value == "true")
+        {
+            return true;
+        }
+        else if (value == "false")
+        {
+            return false;
+        }
+    }
+
+    throw std::invalid_argument{"Element is not a boolean"};
+}
+
+double parseDouble(const nlohmann::json& element,
+                   const std::map<std::string, std::string>& variables)
+{
+    if (element.is_number())
+    {
+        return element.get<double>();
+    }
+
+    if (element.is_string() && !variables.empty())
+    {
+        std::string strValue = parseString(element, true, variables);
+        const char* first = strValue.data();
+        const char* last = strValue.data() + strValue.size();
+        double value;
+        auto [ptr, ec] = std::from_chars(first, last, value);
+        if ((ptr == last) && (ec == std::errc()))
+        {
+            return value;
+        }
+    }
+
+    throw std::invalid_argument{"Element is not a double"};
+}
+
+uint8_t parseHexByte(const nlohmann::json& element,
+                     const std::map<std::string, std::string>& variables)
+{
+    std::string value = parseString(element, true, variables);
+    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, nullptr, 0));
+}
+
+std::vector<uint8_t> parseHexByteArray(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables)
 {
     verifyIsArray(element);
     std::vector<uint8_t> values;
     for (auto& valueElement : element)
     {
-        values.emplace_back(parseHexByte(valueElement));
+        values.emplace_back(parseHexByte(valueElement, variables));
     }
     return values;
 }
 
+int8_t parseInt8(const nlohmann::json& element,
+                 const std::map<std::string, std::string>& variables)
+{
+    int value = parseInteger(element, variables);
+    if ((value < INT8_MIN) || (value > INT8_MAX))
+    {
+        throw std::invalid_argument{"Element is not an 8-bit signed integer"};
+    }
+    return static_cast<int8_t>(value);
+}
+
+int parseInteger(const nlohmann::json& element,
+                 const std::map<std::string, std::string>& variables)
+{
+    if (element.is_number_integer())
+    {
+        return element.get<int>();
+    }
+
+    if (element.is_string() && !variables.empty())
+    {
+        std::string strValue = parseString(element, true, variables);
+        const char* first = strValue.data();
+        const char* last = strValue.data() + strValue.size();
+        int value;
+        auto [ptr, ec] = std::from_chars(first, last, value);
+        if ((ptr == last) && (ec == std::errc()))
+        {
+            return value;
+        }
+    }
+
+    throw std::invalid_argument{"Element is not an integer"};
+}
+
+std::string parseString(const nlohmann::json& element, bool isEmptyValid,
+                        const std::map<std::string, std::string>& variables)
+{
+    if (!element.is_string())
+    {
+        throw std::invalid_argument{"Element is not a string"};
+    }
+    std::string value = element.get<std::string>();
+    internal::expandVariables(value, variables);
+    if (value.empty() && !isEmptyValid)
+    {
+        throw std::invalid_argument{"Element contains an empty string"};
+    }
+    return value;
+}
+
+uint8_t parseUint8(const nlohmann::json& element,
+                   const std::map<std::string, std::string>& variables)
+{
+    int value = parseInteger(element, variables);
+    if ((value < 0) || (value > UINT8_MAX))
+    {
+        throw std::invalid_argument{"Element is not an 8-bit unsigned integer"};
+    }
+    return static_cast<uint8_t>(value);
+}
+
+unsigned int parseUnsignedInteger(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables)
+{
+    int value = parseInteger(element, variables);
+    if (value < 0)
+    {
+        throw std::invalid_argument{"Element is not an unsigned integer"};
+    }
+    return static_cast<unsigned int>(value);
+}
+
+namespace internal
+{
+
+void expandVariables(std::string& value,
+                     const std::map<std::string, std::string>& variables)
+{
+    if (variables.empty())
+    {
+        return;
+    }
+
+    std::smatch results;
+    while (std::regex_search(value, results, VARIABLE_REGEX))
+    {
+        if (results.size() != 2)
+        {
+            throw std::runtime_error{
+                "Unexpected regular expression match result while parsing string"};
+        }
+        const std::string& variable = results[1];
+        auto it = variables.find(variable);
+        if (it == variables.end())
+        {
+            throw std::invalid_argument{"Undefined variable: " + variable};
+        }
+        value.replace(results.position(0), results.length(0), it->second);
+    }
+}
+
+} // namespace internal
+
 } // namespace phosphor::power::json_parser_utils
diff --git a/json_parser_utils.hpp b/json_parser_utils.hpp
index 6dba42b..06ac19f 100644
--- a/json_parser_utils.hpp
+++ b/json_parser_utils.hpp
@@ -18,6 +18,7 @@
 #include <nlohmann/json.hpp>
 
 #include <cstdint>
+#include <map>
 #include <stdexcept>
 #include <string>
 #include <vector>
@@ -26,11 +27,28 @@
  * @namespace json_parser_utils
  *
  * Contains utility functions for parsing JSON data.
+ *
+ * ## Variables
+ * The parsing functions support optional usage of variables. JSON string values
+ * can contain one or more variables. A variable is specified using the format
+ * `${variable_name}`. A variables map is specified to parsing functions that
+ * provides the variable values. Any variable in a JSON string value will be
+ * replaced by the variable value.
+ *
+ * Variables can only appear in a JSON string value. The parsing functions for
+ * other data types, such as integer and double, support a string value if it
+ * contains a variable. After variable expansion, the string is converted to the
+ * expected data type.
  */
 namespace phosphor::power::json_parser_utils
 {
 
 /**
+ * Empty variables map used as a default value for parsing functions.
+ */
+extern const std::map<std::string, std::string> NO_VARIABLES;
+
+/**
  * Returns the specified property of the specified JSON element.
  *
  * Throws an invalid_argument exception if the property does not exist.
@@ -62,22 +80,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @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.get<int>();
-    if ((value < 0) || (value > 7))
-    {
-        throw std::invalid_argument{"Element is not a bit position"};
-    }
-    return static_cast<uint8_t>(value);
-}
+uint8_t parseBitPosition(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing a bit value (0 or 1).
@@ -87,22 +95,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @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.get<int>();
-    if ((value < 0) || (value > 1))
-    {
-        throw std::invalid_argument{"Element is not a bit value"};
-    }
-    return static_cast<uint8_t>(value);
-}
+uint8_t parseBitValue(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing a boolean.
@@ -112,17 +110,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return boolean value
  */
-inline bool parseBoolean(const nlohmann::json& element)
-{
-    // Verify element contains a boolean
-    if (!element.is_boolean())
-    {
-        throw std::invalid_argument{"Element is not a boolean"};
-    }
-    return element.get<bool>();
-}
+bool parseBoolean(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing a double (floating point number).
@@ -132,51 +125,31 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return double value
  */
-inline double parseDouble(const nlohmann::json& element)
-{
-    // Verify element contains a number (integer or floating point)
-    if (!element.is_number())
-    {
-        throw std::invalid_argument{"Element is not a number"};
-    }
-    return element.get<double>();
-}
+double parseDouble(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing a byte value expressed as a hexadecimal
  * string.
  *
  * The JSON number data type does not support the hexadecimal format.  For this
- * reason, hexadecimal byte values are stored as strings in the configuration
- * file.
+ * reason, a hexadecimal byte value is stored in a JSON string.
  *
  * Returns the corresponding C++ uint8_t value.
  *
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return uint8_t value
  */
-inline uint8_t parseHexByte(const nlohmann::json& element)
-{
-    if (!element.is_string())
-    {
-        throw std::invalid_argument{"Element is not a string"};
-    }
-    std::string value = element.get<std::string>();
-
-    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, nullptr, 0));
-}
+uint8_t parseHexByte(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing an array of byte values expressed as a
@@ -187,9 +160,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return vector of uint8_t
  */
-std::vector<uint8_t> parseHexByteArray(const nlohmann::json& element);
+std::vector<uint8_t> parseHexByteArray(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing an 8-bit signed integer.
@@ -199,22 +175,27 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return int8_t value
  */
-inline int8_t parseInt8(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.get<int>();
-    if ((value < INT8_MIN) || (value > INT8_MAX))
-    {
-        throw std::invalid_argument{"Element is not an 8-bit signed integer"};
-    }
-    return static_cast<int8_t>(value);
-}
+int8_t parseInt8(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
+
+/**
+ * Parses a JSON element containing an integer.
+ *
+ * Returns the corresponding C++ int value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param variables variables map used to expand variables in element value
+ * @return integer value
+ */
+int parseInteger(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing a string.
@@ -225,22 +206,12 @@
  *
  * @param element JSON element
  * @param isEmptyValid indicates whether an empty string value is valid
+ * @param variables variables map used to expand variables in element value
  * @return string value
  */
-inline std::string parseString(const nlohmann::json& element,
-                               bool isEmptyValid = false)
-{
-    if (!element.is_string())
-    {
-        throw std::invalid_argument{"Element is not a string"};
-    }
-    std::string value = element.get<std::string>();
-    if (value.empty() && !isEmptyValid)
-    {
-        throw std::invalid_argument{"Element contains an empty string"};
-    }
-    return value;
-}
+std::string parseString(
+    const nlohmann::json& element, bool isEmptyValid = false,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing an 8-bit unsigned integer.
@@ -250,22 +221,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @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.get<int>();
-    if ((value < 0) || (value > UINT8_MAX))
-    {
-        throw std::invalid_argument{"Element is not an 8-bit unsigned integer"};
-    }
-    return static_cast<uint8_t>(value);
-}
+uint8_t parseUint8(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Parses a JSON element containing an unsigned integer.
@@ -275,17 +236,12 @@
  * Throws an exception if parsing fails.
  *
  * @param element JSON element
+ * @param variables variables map used to expand variables in element value
  * @return unsigned int value
  */
-inline unsigned int parseUnsignedInteger(const nlohmann::json& element)
-{
-    // Verify element contains an unsigned integer
-    if (!element.is_number_unsigned())
-    {
-        throw std::invalid_argument{"Element is not an unsigned integer"};
-    }
-    return element.get<unsigned int>();
-}
+unsigned int parseUnsignedInteger(
+    const nlohmann::json& element,
+    const std::map<std::string, std::string>& variables = NO_VARIABLES);
 
 /**
  * Verifies that the specified JSON element is a JSON array.
@@ -337,4 +293,24 @@
     }
 }
 
+namespace internal
+{
+
+/**
+ * Expands any variables that appear in the specified string value.
+ *
+ * Does nothing if the variables map is empty or the value contains no
+ * variables.
+ *
+ * Throws an invalid_argument exception if a variable occurs in the value that
+ * does not exist in the variables map.
+ *
+ * @param value string value in which to perform variable expansion
+ * @param variables variables map containing variable values
+ */
+void expandVariables(std::string& value,
+                     const std::map<std::string, std::string>& variables);
+
+} // namespace internal
+
 } // namespace phosphor::power::json_parser_utils
diff --git a/phosphor-power-sequencer/src/chassis.hpp b/phosphor-power-sequencer/src/chassis.hpp
index 8227953..d40ad7a 100644
--- a/phosphor-power-sequencer/src/chassis.hpp
+++ b/phosphor-power-sequencer/src/chassis.hpp
@@ -40,7 +40,6 @@
 class Chassis
 {
   public:
-    // Specify which compiler-generated methods we want
     Chassis() = delete;
     Chassis(const Chassis&) = delete;
     Chassis(Chassis&&) = delete;
diff --git a/phosphor-power-sequencer/src/system.hpp b/phosphor-power-sequencer/src/system.hpp
index 0ae8e62..e86a096 100644
--- a/phosphor-power-sequencer/src/system.hpp
+++ b/phosphor-power-sequencer/src/system.hpp
@@ -34,7 +34,6 @@
 class System
 {
   public:
-    // Specify which compiler-generated methods we want
     System() = delete;
     System(const System&) = delete;
     System(System&&) = delete;
diff --git a/phosphor-regulators/test/config_file_parser_tests.cpp b/phosphor-regulators/test/config_file_parser_tests.cpp
index 9a68990..c218f5f 100644
--- a/phosphor-regulators/test/config_file_parser_tests.cpp
+++ b/phosphor-regulators/test/config_file_parser_tests.cpp
@@ -693,7 +693,7 @@
     }
     catch (const std::invalid_argument& e)
     {
-        EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
+        EXPECT_STREQ(e.what(), "Element is not an integer");
     }
 
     // Test where fails: inventory_path is invalid: Not a string
@@ -1303,7 +1303,7 @@
     }
     catch (const std::invalid_argument& e)
     {
-        EXPECT_STREQ(e.what(), "Element is not a number");
+        EXPECT_STREQ(e.what(), "Element is not a double");
     }
 
     // Test where fails: actions object is invalid
@@ -3664,7 +3664,7 @@
     }
     catch (const std::invalid_argument& e)
     {
-        EXPECT_STREQ(e.what(), "Element is not a number");
+        EXPECT_STREQ(e.what(), "Element is not a double");
     }
 
     // Test where fails: Required format property not specified
diff --git a/test/json_parser_utils_tests.cpp b/test/json_parser_utils_tests.cpp
index e4f591c..af49977 100644
--- a/test/json_parser_utils_tests.cpp
+++ b/test/json_parser_utils_tests.cpp
@@ -26,6 +26,7 @@
 #include <gtest/gtest.h>
 
 using namespace phosphor::power::json_parser_utils;
+using namespace phosphor::power::json_parser_utils::internal;
 using json = nlohmann::json;
 
 TEST(JSONParserUtilsTests, GetRequiredProperty)
@@ -66,6 +67,14 @@
         EXPECT_EQ(value, 7);
     }
 
+    // Test where works: Variable specified
+    {
+        std::map<std::string, std::string> variables{{"bit_pos", "3"}};
+        const json element = R"( "${bit_pos}" )"_json;
+        uint8_t value = parseBitPosition(element, variables);
+        EXPECT_EQ(value, 3);
+    }
+
     // Test where fails: Element is not an integer
     try
     {
@@ -101,6 +110,19 @@
     {
         EXPECT_STREQ(e.what(), "Element is not a bit position");
     }
+
+    // Test where fails: Variable specified: Value < 0
+    try
+    {
+        std::map<std::string, std::string> variables{{"bit_pos", "-1"}};
+        const json element = R"( "${bit_pos}" )"_json;
+        parseBitPosition(element, variables);
+        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(JSONParserUtilsTests, ParseBitValue)
@@ -119,6 +141,14 @@
         EXPECT_EQ(value, 1);
     }
 
+    // Test where works: Variable specified
+    {
+        std::map<std::string, std::string> variables{{"bit_val", "1"}};
+        const json element = R"( "${bit_val}" )"_json;
+        uint8_t value = parseBitValue(element, variables);
+        EXPECT_EQ(value, 1);
+    }
+
     // Test where fails: Element is not an integer
     try
     {
@@ -154,6 +184,19 @@
     {
         EXPECT_STREQ(e.what(), "Element is not a bit value");
     }
+
+    // Test where fails: Variable specified: Not an integer
+    try
+    {
+        std::map<std::string, std::string> variables{{"bit_val", "one"}};
+        const json element = R"( "${bit_val}" )"_json;
+        parseBitValue(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an integer");
+    }
 }
 
 TEST(JSONParserUtilsTests, ParseBoolean)
@@ -172,6 +215,22 @@
         EXPECT_EQ(value, false);
     }
 
+    // Test where works: Variable specified: true
+    {
+        std::map<std::string, std::string> variables{{"bool_val", "true"}};
+        const json element = R"( "${bool_val}" )"_json;
+        bool value = parseBoolean(element, variables);
+        EXPECT_EQ(value, true);
+    }
+
+    // Test where works: Variable specified: false
+    {
+        std::map<std::string, std::string> variables{{"bool_val", "false"}};
+        const json element = R"( "${bool_val}" )"_json;
+        bool value = parseBoolean(element, variables);
+        EXPECT_EQ(value, false);
+    }
+
     // Test where fails: Element is not a boolean
     try
     {
@@ -183,25 +242,66 @@
     {
         EXPECT_STREQ(e.what(), "Element is not a boolean");
     }
+
+    // Test where fails: Variable specified: Variables map not specified
+    try
+    {
+        const json element = R"( "${bool_val}" )"_json;
+        parseBoolean(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a boolean");
+    }
+
+    // Test where fails: Variable specified: Value is not a boolean
+    try
+    {
+        std::map<std::string, std::string> variables{{"bool_val", "3.2"}};
+        const json element = R"( "${bool_val}" )"_json;
+        parseBoolean(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a boolean");
+    }
 }
 
 TEST(JSONParserUtilsTests, ParseDouble)
 {
-    // Test where works: floating point value
+    // Test where works: Floating point value
     {
         const json element = R"( 1.03 )"_json;
         double value = parseDouble(element);
         EXPECT_EQ(value, 1.03);
     }
 
-    // Test where works: integer value
+    // Test where works: Integer value
     {
-        const json element = R"( 24 )"_json;
+        const json element = R"( -24 )"_json;
         double value = parseDouble(element);
+        EXPECT_EQ(value, -24.0);
+    }
+
+    // Test where works: Variable specified: Floating point value
+    {
+        std::map<std::string, std::string> variables{{"var", "-1.03"}};
+        const json element = R"( "${var}" )"_json;
+        double value = parseDouble(element, variables);
+        EXPECT_EQ(value, -1.03);
+    }
+
+    // Test where works: Variable specified: Integer value
+    {
+        std::map<std::string, std::string> variables{{"var", "24"}};
+        const json element = R"( "${var}" )"_json;
+        double value = parseDouble(element, variables);
         EXPECT_EQ(value, 24.0);
     }
 
-    // Test where fails: Element is not a number
+    // Test where fails: Element is not a double
     try
     {
         const json element = R"( true )"_json;
@@ -210,7 +310,84 @@
     }
     catch (const std::invalid_argument& e)
     {
-        EXPECT_STREQ(e.what(), "Element is not a number");
+        EXPECT_STREQ(e.what(), "Element is not a double");
+    }
+
+    // Test where fails: Variable specified: Variables map not specified
+    try
+    {
+        const json element = R"( "${var}" )"_json;
+        parseDouble(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a double");
+    }
+
+    // Test where fails: Variable specified: Leading whitespace
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", " -1.03"}};
+        const json element = R"( "${var}" )"_json;
+        parseDouble(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a double");
+    }
+
+    // Test where fails: Variable specified: Trailing whitespace
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "-1.03 "}};
+        const json element = R"( "${var}" )"_json;
+        parseDouble(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a double");
+    }
+
+    // Test where fails: Variable specified: Starts with non-number character
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "x-1.03"}};
+        const json element = R"( "${var}" )"_json;
+        parseDouble(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a double");
+    }
+
+    // Test where fails: Variable specified: Ends with non-number character
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "-1.03x"}};
+        const json element = R"( "${var}" )"_json;
+        parseDouble(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a double");
+    }
+
+    // Test where fails: Variable specified: Not a double
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "foo"}};
+        const json element = R"( "${var}" )"_json;
+        parseDouble(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a double");
     }
 }
 
@@ -237,6 +414,14 @@
         EXPECT_EQ(value, 0xf);
     }
 
+    // Test where works: Variable specified
+    {
+        std::map<std::string, std::string> variables{{"var", "ed"}};
+        const json element = R"( "0x${var}" )"_json;
+        uint8_t value = parseHexByte(element, variables);
+        EXPECT_EQ(value, 0xed);
+    }
+
     // Test where fails: "0xfff"
     try
     {
@@ -320,6 +505,19 @@
     {
         EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
     }
+
+    // Test where fails: Variable specified: Not a hex string
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "0xsz"}};
+        const json element = R"( "${var}" )"_json;
+        parseHexByte(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
 }
 
 TEST(JSONParserUtilsTests, ParseHexByteArray)
@@ -332,6 +530,16 @@
         EXPECT_EQ(hexBytes, expected);
     }
 
+    // Test where works: Variables specified
+    {
+        std::map<std::string, std::string> variables{{"var1", "0xCC"},
+                                                     {"var2", "0xFF"}};
+        const json element = R"( [ "${var1}", "${var2}" ] )"_json;
+        std::vector<uint8_t> hexBytes = parseHexByteArray(element, variables);
+        std::vector<uint8_t> expected = {0xcc, 0xff};
+        EXPECT_EQ(hexBytes, expected);
+    }
+
     // Test where fails: Element is not an array
     try
     {
@@ -343,6 +551,20 @@
     {
         EXPECT_STREQ(e.what(), "Element is not an array");
     }
+
+    // Test where fails: Variables specified: Invalid byte value
+    try
+    {
+        std::map<std::string, std::string> variables{{"var1", "0xCC"},
+                                                     {"var2", "99"}};
+        const json element = R"( [ "${var1}", "${var2}" ] )"_json;
+        parseHexByteArray(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not hexadecimal string");
+    }
 }
 
 TEST(JSONParserUtilsTests, ParseInt8)
@@ -361,6 +583,14 @@
         EXPECT_EQ(value, 127);
     }
 
+    // Test where works: Variable specified
+    {
+        std::map<std::string, std::string> variables{{"var", "-23"}};
+        const json element = R"( "${var}" )"_json;
+        int8_t value = parseInt8(element, variables);
+        EXPECT_EQ(value, -23);
+    }
+
     // Test where fails: Element is not an integer
     try
     {
@@ -396,6 +626,148 @@
     {
         EXPECT_STREQ(e.what(), "Element is not an 8-bit signed integer");
     }
+
+    // Test where fails: Variable specified: Value > INT8_MAX
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "128"}};
+        const json element = R"( "${var}" )"_json;
+        parseInt8(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an 8-bit signed integer");
+    }
+}
+
+TEST(JSONParserUtilsTests, ParseInteger)
+{
+    // Test where works: Zero
+    {
+        const json element = R"( 0 )"_json;
+        int value = parseInteger(element);
+        EXPECT_EQ(value, 0);
+    }
+
+    // Test where works: Positive value
+    {
+        const json element = R"( 103 )"_json;
+        int value = parseInteger(element);
+        EXPECT_EQ(value, 103);
+    }
+
+    // Test where works: Negative value
+    {
+        const json element = R"( -24 )"_json;
+        int value = parseInteger(element);
+        EXPECT_EQ(value, -24);
+    }
+
+    // Test where works: Variable specified: Positive value
+    {
+        std::map<std::string, std::string> variables{{"var", "1024"}};
+        const json element = R"( "${var}" )"_json;
+        int value = parseInteger(element, variables);
+        EXPECT_EQ(value, 1024);
+    }
+
+    // Test where works: Variable specified: Negative value
+    {
+        std::map<std::string, std::string> variables{{"var", "-9924"}};
+        const json element = R"( "${var}" )"_json;
+        int value = parseInteger(element, variables);
+        EXPECT_EQ(value, -9924);
+    }
+
+    // Test where fails: Element is not a integer
+    try
+    {
+        const json element = R"( true )"_json;
+        parseInteger(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: Variable specified: Variables map not specified
+    try
+    {
+        const json element = R"( "${var}" )"_json;
+        parseInteger(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: Variable specified: Leading whitespace
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", " -13"}};
+        const json element = R"( "${var}" )"_json;
+        parseInteger(element, variables);
+        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: Variable specified: Trailing whitespace
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "-13 "}};
+        const json element = R"( "${var}" )"_json;
+        parseInteger(element, variables);
+        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: Variable specified: Starts with non-number character
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "x-13"}};
+        const json element = R"( "${var}" )"_json;
+        parseInteger(element, variables);
+        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: Variable specified: Ends with non-number character
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "-13x"}};
+        const json element = R"( "${var}" )"_json;
+        parseInteger(element, variables);
+        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: Variable specified: Not an integer
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "foo"}};
+        const json element = R"( "${var}" )"_json;
+        parseInteger(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an integer");
+    }
 }
 
 TEST(JSONParserUtilsTests, ParseString)
@@ -414,6 +786,22 @@
         EXPECT_EQ(value, "vdd_regulator");
     }
 
+    // Test where works: Variable specified: Empty string
+    {
+        std::map<std::string, std::string> variables{{"var", ""}};
+        const json element = R"( "${var}" )"_json;
+        std::string value = parseString(element, true, variables);
+        EXPECT_EQ(value, "");
+    }
+
+    // Test where works: Variable specified: Non-empty string
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_regulator"}};
+        const json element = R"( "${var}" )"_json;
+        std::string value = parseString(element, false, variables);
+        EXPECT_EQ(value, "vio_regulator");
+    }
+
     // Test where fails: Element is not a string
     try
     {
@@ -437,6 +825,32 @@
     {
         EXPECT_STREQ(e.what(), "Element contains an empty string");
     }
+
+    // Test where fails: Variable specified: Empty string
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", ""}};
+        const json element = R"( "${var}" )"_json;
+        parseString(element, false, variables);
+        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: Variable specified: Variable not defined
+    try
+    {
+        std::map<std::string, std::string> variables{{"var1", "foo"}};
+        const json element = R"( "${var2}" )"_json;
+        parseString(element, false, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Undefined variable: var2");
+    }
 }
 
 TEST(JSONParserUtilsTests, ParseUint8)
@@ -455,6 +869,14 @@
         EXPECT_EQ(value, 255);
     }
 
+    // Test where works: Variable specified
+    {
+        std::map<std::string, std::string> variables{{"var", "19"}};
+        const json element = R"( "${var}" )"_json;
+        uint8_t value = parseUint8(element, variables);
+        EXPECT_EQ(value, 19);
+    }
+
     // Test where fails: Element is not an integer
     try
     {
@@ -490,6 +912,19 @@
     {
         EXPECT_STREQ(e.what(), "Element is not an 8-bit unsigned integer");
     }
+
+    // Test where fails: Variable specified: Value > UINT8_MAX
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "256"}};
+        const json element = R"( "${var}" )"_json;
+        parseUint8(element, variables);
+        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(JSONParserUtilsTests, ParseUnsignedInteger)
@@ -501,6 +936,14 @@
         EXPECT_EQ(value, 1);
     }
 
+    // Test where works: Variable specified
+    {
+        std::map<std::string, std::string> variables{{"var", "25678"}};
+        const json element = R"( "${var}" )"_json;
+        unsigned int value = parseUnsignedInteger(element, variables);
+        EXPECT_EQ(value, 25678);
+    }
+
     // Test where fails: Element is not an integer
     try
     {
@@ -510,7 +953,7 @@
     }
     catch (const std::invalid_argument& e)
     {
-        EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
+        EXPECT_STREQ(e.what(), "Element is not an integer");
     }
 
     // Test where fails: Value < 0
@@ -524,6 +967,19 @@
     {
         EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
     }
+
+    // Test where fails: Variable specified: Value < 0
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "-23"}};
+        const json element = R"( "${var}" )"_json;
+        parseUnsignedInteger(element, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not an unsigned integer");
+    }
 }
 
 TEST(JSONParserUtilsTests, VerifyIsArray)
@@ -599,3 +1055,154 @@
         EXPECT_STREQ(e.what(), "Element contains an invalid property");
     }
 }
+
+TEST(JSONParserUtilsTests, ExpandVariables)
+{
+    // Test where works: Single variable: Variable is entire value: Lower case
+    // in variable name
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_regulator"}};
+        std::string value{"${var}"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "vio_regulator");
+    }
+
+    // Test where works: Multiple variables: Variables are part of value: Upper
+    // case and underscore in variable name
+    {
+        std::map<std::string, std::string> variables{
+            {"CHASSIS_NUMBER", "1"}, {"REGULATOR", "vcs_vio"}, {"RAIL", "vio"}};
+        std::string value{
+            "chassis${CHASSIS_NUMBER}_${REGULATOR}_regulator_${RAIL}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "chassis1_vcs_vio_regulator_vio_rail");
+    }
+
+    // Test where works: Variable at start of value: Number in variable name
+    {
+        std::map<std::string, std::string> variables{{"var1", "vio_regulator"}};
+        std::string value{"${var1}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "vio_regulator_rail");
+    }
+
+    // Test where works: Variable at end of value
+    {
+        std::map<std::string, std::string> variables{{"chassis_number", "3"}};
+        std::string value{
+            "/xyz/openbmc_project/inventory/system/chassis${chassis_number}"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "/xyz/openbmc_project/inventory/system/chassis3");
+    }
+
+    // Test where works: Variable has empty value: Start of value
+    {
+        std::map<std::string, std::string> variables{{"chassis_prefix", ""}};
+        std::string value{"${chassis_prefix}vio_regulator"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "vio_regulator");
+    }
+
+    // Test where works: Variable has empty value: Middle of value
+    {
+        std::map<std::string, std::string> variables{{"chassis_number", ""}};
+        std::string value{"c${chassis_number}_vio_regulator"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "c_vio_regulator");
+    }
+
+    // Test where works: Variable has empty value: End of value
+    {
+        std::map<std::string, std::string> variables{{"chassis_number", ""}};
+        std::string value{
+            "/xyz/openbmc_project/inventory/system/chassis${chassis_number}"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "/xyz/openbmc_project/inventory/system/chassis");
+    }
+
+    // Test where works: No variables specified
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_regulator"}};
+        std::string value{"vcs_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "vcs_rail");
+    }
+
+    // Test where works: Nested variable expansion
+    {
+        std::map<std::string, std::string> variables{{"var1", "${var2}"},
+                                                     {"var2", "vio_reg"}};
+        std::string value{"${var1}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "vio_reg_rail");
+    }
+
+    // Test where fails: Variables map is empty
+    {
+        std::map<std::string, std::string> variables{};
+        std::string value{"${var}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "${var}_rail");
+    }
+
+    // Test where fails: Variable missing $
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_reg"}};
+        std::string value{"{var}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "{var}_rail");
+    }
+
+    // Test where fails: Variable missing {
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_reg"}};
+        std::string value{"$var}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "$var}_rail");
+    }
+
+    // Test where fails: Variable missing }
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_reg"}};
+        std::string value{"${var_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "${var_rail");
+    }
+
+    // Test where fails: Variable missing name
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_reg"}};
+        std::string value{"${}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "${}_rail");
+    }
+
+    // Test where fails: Variable name has invalid characters
+    {
+        std::map<std::string, std::string> variables{{"var-2", "vio_reg"}};
+        std::string value{"${var-2}_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "${var-2}_rail");
+    }
+
+    // Test where fails: Variable has unexpected whitespace
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_reg"}};
+        std::string value{"${ var }_rail"};
+        expandVariables(value, variables);
+        EXPECT_EQ(value, "${ var }_rail");
+    }
+
+    // Test where fails: Undefined variable
+    try
+    {
+        std::map<std::string, std::string> variables{{"var", "vio_reg"}};
+        std::string value{"${foo}_rail"};
+        expandVariables(value, variables);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Undefined variable: foo");
+    }
+}