pseq: Create standalone config file parser

Create a standalone JSON configuration file parser for the
phosphor-power-sequencer application.  Support the new config file
properties in the parser.

Separating the config file parser from the rest of the code makes it
easier to implement and test.

Create automated test cases to verify the parser functions.

Change-Id: Ia753ffadf395359899964a612e479b00377ad495
Signed-off-by: Shawn McCarney <shawnmm@us.ibm.com>
diff --git a/phosphor-power-sequencer/src/config_file_parser.cpp b/phosphor-power-sequencer/src/config_file_parser.cpp
new file mode 100644
index 0000000..db055d1
--- /dev/null
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -0,0 +1,173 @@
+/**
+ * Copyright © 2024 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 "config_file_parser.hpp"
+
+#include "config_file_parser_error.hpp"
+
+#include <exception>
+#include <fstream>
+#include <optional>
+
+using json = nlohmann::json;
+
+namespace phosphor::power::sequencer::config_file_parser
+{
+
+std::vector<std::unique_ptr<Rail>> parse(const std::filesystem::path& pathName)
+{
+    try
+    {
+        // Use standard JSON parser to create tree of JSON elements
+        std::ifstream file{pathName};
+        json rootElement = json::parse(file);
+
+        // Parse tree of JSON elements and return corresponding C++ objects
+        return internal::parseRoot(rootElement);
+    }
+    catch (const std::exception& e)
+    {
+        throw ConfigFileParserError{pathName, e.what()};
+    }
+}
+
+namespace internal
+{
+
+GPIO parseGPIO(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Required line property
+    const json& lineElement = getRequiredProperty(element, "line");
+    unsigned int line = parseUnsignedInteger(lineElement);
+    ++propertyCount;
+
+    // Optional active_low property
+    bool activeLow{false};
+    auto activeLowIt = element.find("active_low");
+    if (activeLowIt != element.end())
+    {
+        activeLow = parseBoolean(*activeLowIt);
+        ++propertyCount;
+    }
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return GPIO(line, activeLow);
+}
+
+std::unique_ptr<Rail> parseRail(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Required name property
+    const json& nameElement = getRequiredProperty(element, "name");
+    std::string name = parseString(nameElement);
+    ++propertyCount;
+
+    // Optional presence property
+    std::optional<std::string> presence{};
+    auto presenceIt = element.find("presence");
+    if (presenceIt != element.end())
+    {
+        presence = parseString(*presenceIt);
+        ++propertyCount;
+    }
+
+    // Optional page property
+    std::optional<uint8_t> page{};
+    auto pageIt = element.find("page");
+    if (pageIt != element.end())
+    {
+        page = parseUint8(*pageIt);
+        ++propertyCount;
+    }
+
+    // Optional check_status_vout property
+    bool checkStatusVout{false};
+    auto checkStatusVoutIt = element.find("check_status_vout");
+    if (checkStatusVoutIt != element.end())
+    {
+        checkStatusVout = parseBoolean(*checkStatusVoutIt);
+        ++propertyCount;
+    }
+
+    // Optional compare_voltage_to_limits property
+    bool compareVoltageToLimits{false};
+    auto compareVoltageToLimitsIt = element.find("compare_voltage_to_limits");
+    if (compareVoltageToLimitsIt != element.end())
+    {
+        compareVoltageToLimits = parseBoolean(*compareVoltageToLimitsIt);
+        ++propertyCount;
+    }
+
+    // Optional gpio property
+    std::optional<GPIO> gpio{};
+    auto gpioIt = element.find("gpio");
+    if (gpioIt != element.end())
+    {
+        gpio = parseGPIO(*gpioIt);
+        ++propertyCount;
+    }
+
+    // If check_status_vout or compare_voltage_to_limits property is true,
+    // the page property is required; verify page was specified
+    if ((checkStatusVout || compareVoltageToLimits) && !page.has_value())
+    {
+        throw std::invalid_argument{"Required property missing: page"};
+    }
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return std::make_unique<Rail>(name, presence, page, checkStatusVout,
+                                  compareVoltageToLimits, gpio);
+}
+
+std::vector<std::unique_ptr<Rail>> parseRailArray(const json& element)
+{
+    verifyIsArray(element);
+    std::vector<std::unique_ptr<Rail>> rails;
+    for (auto& railElement : element)
+    {
+        rails.emplace_back(parseRail(railElement));
+    }
+    return rails;
+}
+
+std::vector<std::unique_ptr<Rail>> parseRoot(const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Required rails property
+    const json& railsElement = getRequiredProperty(element, "rails");
+    std::vector<std::unique_ptr<Rail>> rails = parseRailArray(railsElement);
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return rails;
+}
+
+} // namespace internal
+
+} // namespace phosphor::power::sequencer::config_file_parser
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
new file mode 100644
index 0000000..3764b7c
--- /dev/null
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -0,0 +1,265 @@
+/**
+ * Copyright © 2024 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 "rail.hpp"
+
+#include <nlohmann/json.hpp>
+
+#include <cstdint>
+#include <filesystem>
+#include <stdexcept>
+#include <string>
+#include <vector>
+
+namespace phosphor::power::sequencer::config_file_parser
+{
+
+/**
+ * Parses the specified JSON configuration file.
+ *
+ * Returns the corresponding C++ Rail objects.
+ *
+ * Throws a ConfigFileParserError if an error occurs.
+ *
+ * @param pathName configuration file path name
+ * @return vector of Rail objects
+ */
+std::vector<std::unique_ptr<Rail>> parse(const std::filesystem::path& pathName);
+
+/*
+ * Internal implementation details for parse()
+ */
+namespace internal
+{
+
+/**
+ * Returns the specified property of the specified JSON element.
+ *
+ * Throws an invalid_argument exception if the property does not exist.
+ *
+ * @param element JSON element
+ * @param property property name
+ */
+#pragma GCC diagnostic push
+#if __GNUC__ == 13
+#pragma GCC diagnostic ignored "-Wdangling-reference"
+#endif
+inline const nlohmann::json& getRequiredProperty(const nlohmann::json& element,
+                                                 const std::string& property)
+{
+    auto it = element.find(property);
+    if (it == element.end())
+    {
+        throw std::invalid_argument{"Required property missing: " + property};
+    }
+    return *it;
+}
+#pragma GCC diagnostic pop
+
+/**
+ * Parses a JSON element containing a boolean.
+ *
+ * Returns the corresponding C++ boolean value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @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>();
+}
+
+/**
+ * Parses a JSON element containing a GPIO.
+ *
+ * Returns the corresponding C++ GPIO object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return GPIO object
+ */
+GPIO parseGPIO(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing a rail.
+ *
+ * Returns the corresponding C++ Rail object.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return Rail object
+ */
+std::unique_ptr<Rail> parseRail(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing an array of rails.
+ *
+ * Returns the corresponding C++ Rail objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Rail objects
+ */
+std::vector<std::unique_ptr<Rail>>
+    parseRailArray(const nlohmann::json& element);
+
+/**
+ * Parses the JSON root element of the entire configuration file.
+ *
+ * Returns the corresponding C++ Rail objects.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return vector of Rail objects
+ */
+std::vector<std::unique_ptr<Rail>> parseRoot(const nlohmann::json& element);
+
+/**
+ * Parses a JSON element containing a string.
+ *
+ * Returns the corresponding C++ string.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @param isEmptyValid indicates whether an empty string value is valid
+ * @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;
+}
+
+/**
+ * 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.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);
+}
+
+/**
+ * Parses a JSON element containing an unsigned integer.
+ *
+ * Returns the corresponding C++ unsigned int value.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @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>();
+}
+
+/**
+ * Verifies that the specified JSON element is a JSON array.
+ *
+ * Throws an invalid_argument exception if the element is not an array.
+ *
+ * @param element JSON element
+ */
+inline void verifyIsArray(const nlohmann::json& element)
+{
+    if (!element.is_array())
+    {
+        throw std::invalid_argument{"Element is not an array"};
+    }
+}
+
+/**
+ * Verifies that the specified JSON element is a JSON object.
+ *
+ * Throws an invalid_argument exception if the element is not an object.
+ *
+ * @param element JSON element
+ */
+inline void verifyIsObject(const nlohmann::json& element)
+{
+    if (!element.is_object())
+    {
+        throw std::invalid_argument{"Element is not an object"};
+    }
+}
+
+/**
+ * Verifies that the specified JSON element contains the expected number of
+ * properties.
+ *
+ * Throws an invalid_argument exception if the element contains a different
+ * number of properties.  This indicates the element contains an invalid
+ * property.
+ *
+ * @param element JSON element
+ * @param expectedCount expected number of properties in element
+ */
+inline void verifyPropertyCount(const nlohmann::json& element,
+                                unsigned int expectedCount)
+{
+    if (element.size() != expectedCount)
+    {
+        throw std::invalid_argument{"Element contains an invalid property"};
+    }
+}
+
+} // namespace internal
+
+} // namespace phosphor::power::sequencer::config_file_parser
diff --git a/phosphor-power-sequencer/src/config_file_parser_error.hpp b/phosphor-power-sequencer/src/config_file_parser_error.hpp
new file mode 100644
index 0000000..4c77952
--- /dev/null
+++ b/phosphor-power-sequencer/src/config_file_parser_error.hpp
@@ -0,0 +1,85 @@
+/**
+ * Copyright © 2024 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 <exception>
+#include <filesystem>
+#include <string>
+
+namespace phosphor::power::sequencer
+{
+
+/**
+ * @class ConfigFileParserError
+ *
+ * An error that occurred while parsing a JSON configuration file.
+ */
+class ConfigFileParserError : public std::exception
+{
+  public:
+    // Specify which compiler-generated methods we want
+    ConfigFileParserError() = delete;
+    ConfigFileParserError(const ConfigFileParserError&) = default;
+    ConfigFileParserError(ConfigFileParserError&&) = default;
+    ConfigFileParserError& operator=(const ConfigFileParserError&) = default;
+    ConfigFileParserError& operator=(ConfigFileParserError&&) = default;
+    virtual ~ConfigFileParserError() = default;
+
+    /**
+     * Constructor.
+     *
+     * @param pathName Configuration file path name
+     * @param error Error message
+     */
+    explicit ConfigFileParserError(const std::filesystem::path& pathName,
+                                   const std::string& error) :
+        pathName{pathName},
+        error{"ConfigFileParserError: " + pathName.string() + ": " + error}
+    {}
+
+    /**
+     * Returns the configuration file path name.
+     *
+     * @return path name
+     */
+    const std::filesystem::path& getPathName()
+    {
+        return pathName;
+    }
+
+    /**
+     * Returns the description of this error.
+     *
+     * @return error description
+     */
+    const char* what() const noexcept override
+    {
+        return error.c_str();
+    }
+
+  private:
+    /**
+     * Configuration file path name.
+     */
+    const std::filesystem::path pathName;
+
+    /**
+     * Error message.
+     */
+    const std::string error{};
+};
+
+} // namespace phosphor::power::sequencer
diff --git a/phosphor-power-sequencer/src/meson.build b/phosphor-power-sequencer/src/meson.build
index de691c9..faab116 100644
--- a/phosphor-power-sequencer/src/meson.build
+++ b/phosphor-power-sequencer/src/meson.build
@@ -3,6 +3,18 @@
     '../..'
 )
 
+phosphor_power_sequencer_library = static_library(
+    'phosphor-power-sequencer',
+    'config_file_parser.cpp',
+    implicit_include_directories: false,
+    dependencies: [
+        nlohmann_json_dep
+    ],
+    include_directories: [
+        phosphor_power_sequencer_include_directories
+    ]
+)
+
 phosphor_power_sequencer = executable(
     'phosphor-power-control',
     'power_control_main.cpp',