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