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.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