pseq: Parsing support for chassis_template element

Add JSON parsing support for the new chassis_template element in the
phosphor-power-sequencer configuration file.

Also move the type alias 'json = nlohmann::json' from
config_file_parse.cpp to config_file_parser.hpp. This simplifies the
function declarations and allows them to textually match the function
definitions.

Tested:
* Ran automated tests.

Change-Id: I666ae0a6382ffec6643d3e268117d006b6c4dd2a
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
index 4539eb7..6d9b169 100644
--- a/phosphor-power-sequencer/src/config_file_parser.cpp
+++ b/phosphor-power-sequencer/src/config_file_parser.cpp
@@ -29,7 +29,6 @@
 
 using namespace phosphor::power::json_parser_utils;
 using ConfigFileParserError = phosphor::power::util::ConfigFileParserError;
-using json = nlohmann::json;
 namespace fs = std::filesystem;
 
 namespace phosphor::power::sequencer::config_file_parser
@@ -95,6 +94,44 @@
 namespace internal
 {
 
+std::tuple<std::string, JSONRefWrapper> parseChassisTemplate(
+    const json& element)
+{
+    verifyIsObject(element);
+    unsigned int propertyCount{0};
+
+    // Optional comments property; value not stored
+    if (element.contains("comments"))
+    {
+        ++propertyCount;
+    }
+
+    // Required id property
+    const json& idElement = getRequiredProperty(element, "id");
+    std::string id = parseString(idElement);
+    ++propertyCount;
+
+    // Required number property
+    // Just verify it exists; cannot be parsed without variable values
+    getRequiredProperty(element, "number");
+    ++propertyCount;
+
+    // Required inventory_path property
+    // Just verify it exists; cannot be parsed without variable values
+    getRequiredProperty(element, "inventory_path");
+    ++propertyCount;
+
+    // Required power_sequencers property
+    // Just verify it exists; cannot be parsed without variable values
+    getRequiredProperty(element, "power_sequencers");
+    ++propertyCount;
+
+    // Verify no invalid properties exist
+    verifyPropertyCount(element, propertyCount);
+
+    return {id, JSONRefWrapper{element}};
+}
+
 GPIO parseGPIO(const json& element,
                const std::map<std::string, std::string>& variables)
 {
diff --git a/phosphor-power-sequencer/src/config_file_parser.hpp b/phosphor-power-sequencer/src/config_file_parser.hpp
index fcae99a..b2a18ca 100644
--- a/phosphor-power-sequencer/src/config_file_parser.hpp
+++ b/phosphor-power-sequencer/src/config_file_parser.hpp
@@ -23,12 +23,15 @@
 
 #include <cstdint>
 #include <filesystem>
+#include <functional> // for reference_wrapper
 #include <map>
 #include <memory>
 #include <string>
 #include <tuple>
 #include <vector>
 
+using json = nlohmann::json;
+
 namespace phosphor::power::sequencer::config_file_parser
 {
 
@@ -80,6 +83,21 @@
 namespace internal
 {
 
+using JSONRefWrapper = std::reference_wrapper<const json>;
+
+/**
+ * Parses a JSON element containing a chassis_template object.
+ *
+ * Returns the template ID and a C++ reference_wrapper to the JSON element.
+ *
+ * Throws an exception if parsing fails.
+ *
+ * @param element JSON element
+ * @return template ID and reference_wrapper to JSON element
+ */
+std::tuple<std::string, JSONRefWrapper> parseChassisTemplate(
+    const json& element);
+
 /**
  * Parses a JSON element containing a GPIO.
  *
@@ -91,7 +109,7 @@
  * @param variables variables map used to expand variables in element value
  * @return GPIO object
  */
-GPIO parseGPIO(const nlohmann::json& element,
+GPIO parseGPIO(const json& element,
                const std::map<std::string, std::string>& variables);
 
 /**
@@ -106,8 +124,7 @@
  * @return tuple containing bus and address
  */
 std::tuple<uint8_t, uint16_t> parseI2CInterface(
-    const nlohmann::json& element,
-    const std::map<std::string, std::string>& variables);
+    const json& element, const std::map<std::string, std::string>& variables);
 
 /**
  * Parses a JSON element containing a power_sequencer object.
@@ -118,12 +135,12 @@
  *
  * @param element JSON element
  * @param variables variables map used to expand variables in element value
- * @param services System services like hardware presence and the journal
+ * @param services system services like hardware presence and the journal
  * @return PowerSequencerDevice object
  */
 std::unique_ptr<PowerSequencerDevice> parsePowerSequencer(
-    const nlohmann::json& element,
-    const std::map<std::string, std::string>& variables, Services& services);
+    const json& element, const std::map<std::string, std::string>& variables,
+    Services& services);
 
 /**
  * Parses a JSON element containing an array of power_sequencer objects.
@@ -134,12 +151,12 @@
  *
  * @param element JSON element
  * @param variables variables map used to expand variables in element value
- * @param services System services like hardware presence and the journal
+ * @param services system services like hardware presence and the journal
  * @return vector of PowerSequencerDevice objects
  */
 std::vector<std::unique_ptr<PowerSequencerDevice>> parsePowerSequencerArray(
-    const nlohmann::json& element,
-    const std::map<std::string, std::string>& variables, Services& services);
+    const json& element, const std::map<std::string, std::string>& variables,
+    Services& services);
 
 /**
  * Parses a JSON element containing a rail.
@@ -153,8 +170,7 @@
  * @return Rail object
  */
 std::unique_ptr<Rail> parseRail(
-    const nlohmann::json& element,
-    const std::map<std::string, std::string>& variables);
+    const json& element, const std::map<std::string, std::string>& variables);
 
 /**
  * Parses a JSON element containing an array of rails.
@@ -168,8 +184,7 @@
  * @return vector of Rail objects
  */
 std::vector<std::unique_ptr<Rail>> parseRailArray(
-    const nlohmann::json& element,
-    const std::map<std::string, std::string>& variables);
+    const json& element, const std::map<std::string, std::string>& variables);
 
 /**
  * Parses the JSON root element of the entire configuration file.
@@ -181,7 +196,7 @@
  * @param element JSON element
  * @return vector of Rail objects
  */
-std::vector<std::unique_ptr<Rail>> parseRoot(const nlohmann::json& element);
+std::vector<std::unique_ptr<Rail>> parseRoot(const json& element);
 
 } // namespace internal
 
diff --git a/phosphor-power-sequencer/test/config_file_parser_tests.cpp b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
index c1e8426..a4a8040 100644
--- a/phosphor-power-sequencer/test/config_file_parser_tests.cpp
+++ b/phosphor-power-sequencer/test/config_file_parser_tests.cpp
@@ -326,6 +326,178 @@
     }
 }
 
+TEST(ConfigFileParserTests, ParseChassisTemplate)
+{
+    // Test where works: comments specified
+    {
+        const json element = R"(
+            {
+              "comments": [ "This is a template for the foo chassis type",
+                            "Chassis contains a UCD90320 power sequencer" ],
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": [
+                {
+                  "type": "UCD90320",
+                  "i2c_interface": { "bus": "${bus}", "address": "0x11" },
+                  "power_control_gpio_name": "power-chassis${chassis_number}-control",
+                  "power_good_gpio_name": "power-chassis${chassis_number}-good",
+                  "rails": [ { "name": "VDD_CPU0" }, { "name": "VCS_CPU1" } ]
+                }
+              ]
+            }
+        )"_json;
+        auto [id, jsonRef] = parseChassisTemplate(element);
+        EXPECT_EQ(id, "foo_chassis");
+        EXPECT_EQ(jsonRef.get()["number"], "${chassis_number}");
+        EXPECT_EQ(jsonRef.get()["power_sequencers"].size(), 1);
+        EXPECT_EQ(jsonRef.get()["power_sequencers"][0]["type"], "UCD90320");
+    }
+
+    // Test where works: comments not specified
+    {
+        const json element = R"(
+            {
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        auto [id, jsonRef] = parseChassisTemplate(element);
+        EXPECT_EQ(id, "foo_chassis");
+        EXPECT_EQ(jsonRef.get()["number"], "${chassis_number}");
+        EXPECT_EQ(
+            jsonRef.get()["inventory_path"],
+            "/xyz/openbmc_project/inventory/system/chassis${chassis_number}");
+        EXPECT_EQ(jsonRef.get()["power_sequencers"].size(), 0);
+    }
+
+    // Test where fails: Element is not an object
+    try
+    {
+        const json element = R"( [ "vdda", "vddb" ] )"_json;
+        parseChassisTemplate(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: Required id property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        parseChassisTemplate(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: id");
+    }
+
+    // Test where fails: Required number property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "id": "foo_chassis",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        parseChassisTemplate(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: number");
+    }
+
+    // Test where fails: Required inventory_path property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        parseChassisTemplate(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: inventory_path");
+    }
+
+    // Test where fails: Required power_sequencers property not specified
+    try
+    {
+        const json element = R"(
+            {
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}"
+            }
+        )"_json;
+        parseChassisTemplate(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Required property missing: power_sequencers");
+    }
+
+    // Test where fails: id value is invalid
+    try
+    {
+        const json element = R"(
+            {
+              "id": 13,
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": []
+            }
+        )"_json;
+        parseChassisTemplate(element);
+        ADD_FAILURE() << "Should not have reached this line.";
+    }
+    catch (const std::invalid_argument& e)
+    {
+        EXPECT_STREQ(e.what(), "Element is not a string");
+    }
+
+    // Test where fails: Invalid property specified
+    try
+    {
+        const json element = R"(
+            {
+              "id": "foo_chassis",
+              "number": "${chassis_number}",
+              "inventory_path": "/xyz/openbmc_project/inventory/system/chassis${chassis_number}",
+              "power_sequencers": [],
+              "foo": "bar"
+            }
+        )"_json;
+        parseChassisTemplate(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(ConfigFileParserTests, ParseGPIO)
 {
     // Test where works: Only required properties specified