Load the associations definitions

Parse the JSON file to load in the _associations
data structures.  Any failures will cause an exception
to be thrown that will crash the app.

The JSON looks like:

[
    {
        "path": "The relative path of the inventory object to create the
                 org.openbmc.Associations interface on."
        "endpoints":
        [
            {
                "types":
                {
                    "fType": "The forward association type."
                    "rType": "The reverse association type."
                },
                "paths":
                [
                    "The list of association endpoints for this
                     inventory path and association type."
                ]
            }
        ]
    }
]

Change-Id: I098fdc607f0c3ab2861f9b33e3e0d46e4989bd7a
Signed-off-by: Matt Spinler <spinler@us.ibm.com>
diff --git a/.gitignore b/.gitignore
index cf6039d..44250a6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -31,3 +31,13 @@
 stamp-h1
 Makefile.extra
 extra_ifaces.cpp
+test-driver
+*.log
+*.trs
+*-libtool
+associations-test
+interface-ops-test
+manager-test
+serialize-test
+types-test
+utils-test
diff --git a/association_manager.cpp b/association_manager.cpp
index 41f135d..91bb309 100644
--- a/association_manager.cpp
+++ b/association_manager.cpp
@@ -1,5 +1,10 @@
 #include "association_manager.hpp"
 
+#include <filesystem>
+#include <fstream>
+#include <nlohmann/json.hpp>
+#include <phosphor-logging/log.hpp>
+
 namespace phosphor
 {
 namespace inventory
@@ -8,10 +13,69 @@
 {
 namespace associations
 {
+using namespace phosphor::logging;
+using sdbusplus::exception::SdBusError;
 
 Manager::Manager(sdbusplus::bus::bus& bus, const std::string& jsonPath) :
     _bus(bus), _jsonFile(jsonPath)
 {
+    load();
+}
+
+/**
+ * @brief Throws an exception if 'num' is zero. Used for JSON
+ *        sanity checking.
+ *
+ * @param[in] num - the number to check
+ */
+void throwIfZero(int num)
+{
+    if (!num)
+    {
+        throw std::invalid_argument("Invalid empty field in JSON");
+    }
+}
+
+void Manager::load()
+{
+    // Load the contents of _jsonFile into _associations and throw
+    // an exception on any problem.
+
+    std::ifstream file{_jsonFile};
+
+    auto json = nlohmann::json::parse(file, nullptr, true);
+
+    const std::string root{INVENTORY_ROOT};
+
+    for (const auto& jsonAssoc : json)
+    {
+        // Only add the slash if necessary
+        std::string path = jsonAssoc.at("path");
+        throwIfZero(path.size());
+        if (path.front() != '/')
+        {
+            path = root + "/" + path;
+        }
+        else
+        {
+            path = root + path;
+        }
+
+        auto& assocEndpoints = _associations[path];
+
+        for (const auto& endpoint : jsonAssoc.at("endpoints"))
+        {
+            std::string ftype = endpoint.at("types").at("fType");
+            std::string rtype = endpoint.at("types").at("rType");
+            throwIfZero(ftype.size());
+            throwIfZero(rtype.size());
+            Types types{std::move(ftype), std::move(rtype)};
+
+            Paths paths = endpoint.at("paths");
+            throwIfZero(paths.size());
+            assocEndpoints.emplace_back(std::move(types), std::move(paths));
+        }
+    }
 }
 
 void Manager::createAssociations(const std::string& objectPath)
diff --git a/association_manager.hpp b/association_manager.hpp
index 1aff7e0..0196c36 100644
--- a/association_manager.hpp
+++ b/association_manager.hpp
@@ -13,6 +13,17 @@
 namespace associations
 {
 
+static constexpr auto forwardTypePos = 0;
+static constexpr auto reverseTypePos = 1;
+using Types = std::tuple<std::string, std::string>;
+using Paths = std::vector<std::string>;
+
+static constexpr auto typesPos = 0;
+static constexpr auto pathsPos = 1;
+using EndpointsEntry = std::vector<std::tuple<Types, Paths>>;
+
+using AssociationMap = std::map<std::string, EndpointsEntry>;
+
 /**
  * @class Manager
  *
@@ -66,8 +77,33 @@
      */
     void createAssociations(const std::string& objectPath);
 
+    /**
+     * @brief Returned the association configuration.
+     *        Used for testing.
+     *
+     * @return AssociationMap& - the association config
+     */
+    const AssociationMap& getAssociationsConfig()
+    {
+        return _associations;
+    }
+
   private:
     /**
+     *  @brief Loads the association YAML into the _associations data
+     *         structure.  This file is optional, so if it doesn't exist
+     *         it will just not load anything.
+     */
+    void load();
+
+    /**
+     * @brief The map of association data that is loaded from its
+     *        JSON definition.  Association D-Bus objects will be
+     *        created from this data.
+     */
+    AssociationMap _associations;
+
+    /**
      * @brief The sdbusplus bus object.
      */
     sdbusplus::bus::bus& _bus;
diff --git a/test/Makefile.am b/test/Makefile.am
index 393ff0a..a53747b 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -39,6 +39,18 @@
 manager_test_LDFLAGS = ${OESDK_TESTCASE_FLAGS}
 check_PROGRAMS += manager-test
 
+associations_test_SOURCES = associations_test.cpp
+associations_test_CFLAGS = \
+	${GTEST_CFLAGS} ${GMOCK_CFLAGS} $(SDBUSPLUS_CFLAGS) \
+	$(PHOSPHOR_DBUS_INTERACES_CFLAGS)
+associations_test_LDADD = \
+	${GTEST_LIBS} ${GMOCK_LIBS} ${SDBUSPLUS_LIBS} -lstdc++fs \
+	$(PHOSPHOR_DBUS_INTERFACES_LIBS) \
+	$(top_builddir)/association_manager.o
+
+associations_test_LDFLAGS = ${OESDK_TESTCASE_FLAGS}
+check_PROGRAMS += associations-test
+
 extra_yamldir=$(top_srcdir)/example/extra_interfaces.d
 
 phosphor_inventory_test_SOURCES = test.cpp
diff --git a/test/associations_test.cpp b/test/associations_test.cpp
new file mode 100644
index 0000000..357659d
--- /dev/null
+++ b/test/associations_test.cpp
@@ -0,0 +1,302 @@
+#include "association_manager.hpp"
+
+#include <filesystem>
+#include <fstream>
+
+#include <gtest/gtest.h>
+
+using namespace phosphor::inventory::manager::associations;
+namespace fs = std::filesystem;
+
+static const auto goodJson = R"(
+[
+    {
+        "path": "system/PS0",
+        "endpoints":
+        [
+            {
+                "types":
+                {
+                    "rType": "inventory",
+                    "fType": "sensors"
+                },
+                "paths":
+                [
+                    "power/ps0_input_power",
+                    "voltage/ps0_input_voltage",
+                    "current/ps0_output_current",
+                    "voltage/ps0_output_voltage"
+                ]
+            },
+            {
+                "types":
+                {
+                    "rType": "inventory",
+                    "fType": "fans"
+                },
+                "paths":
+                [
+                    "fan_tach/ps0_fan"
+                ]
+            }
+        ]
+    },
+    {
+        "path": "system/fan42",
+        "endpoints":
+        [
+            {
+                "types":
+                {
+                    "rType": "inventory",
+                    "fType": "sensors"
+                },
+                "paths":
+                [
+                    "fan_tach/fan42"
+                ]
+            },
+            {
+                "types":
+                {
+                    "rType": "inventory",
+                    "fType": "led"
+                },
+                "paths":
+                [
+                    "led/fan42"
+                ]
+            }
+        ]
+    }
+])";
+
+// Malformed JSON
+static const auto badJson0 = R"(
+    "hello": world
+})";
+
+// Uses 'blah' instead of 'paths'
+static const auto badJson1 = R"(
+[
+    {
+        "blah": "system/PS0",
+        "endpoints":
+        [
+            {
+                "types":
+                {
+                    "fType": "inventory",
+                    "rType": "sensors"
+                },
+                "paths":
+                [
+                    "ps0_input_power",
+                ]
+            }
+        ]
+    }
+])";
+
+// Uses 'blah' instead of 'rType'
+static const auto badJson2 = R"(
+[
+    {
+        "paths": "system/PS0",
+        "endpoints":
+        [
+            {
+                "types":
+                {
+                    "blah": "inventory",
+                    "fType": "sensors"
+                },
+                "paths":
+                [
+                    "ps0_input_power",
+                ]
+            }
+        ]
+    }
+])";
+
+// Missing the endpoints/paths array
+static const auto badJson3 = R"(
+[
+    {
+        "paths": "system/PS0",
+        "endpoints":
+        [
+            {
+                "types":
+                {
+                    "rType": "inventory",
+                    "fType": "sensors"
+                }
+            }
+        ]
+    }
+])";
+
+class AssocsTest : public ::testing::Test
+{
+  protected:
+    AssocsTest() : ::testing::Test(), bus(sdbusplus::bus::new_default())
+    {
+    }
+
+    fs::path jsonDir;
+    sdbusplus::bus::bus bus;
+
+    virtual void SetUp()
+    {
+        char dir[] = {"assocTestXXXXXX"};
+        jsonDir = mkdtemp(dir);
+    }
+
+    virtual void TearDown()
+    {
+        fs::remove_all(jsonDir);
+    }
+
+    std::string writeFile(const char* data)
+    {
+        fs::path path = jsonDir / "associations.json";
+
+        std::ofstream f{path};
+        f << data;
+        f.close();
+
+        return path;
+    }
+};
+
+TEST_F(AssocsTest, TEST_NO_JSON)
+{
+    try
+    {
+        Manager m{bus};
+        EXPECT_TRUE(false);
+    }
+    catch (std::exception& e)
+    {
+    }
+}
+
+TEST_F(AssocsTest, TEST_GOOD_JSON)
+{
+    auto path = writeFile(goodJson);
+    Manager m(bus, path);
+
+    const auto& a = m.getAssociationsConfig();
+    EXPECT_EQ(a.size(), 2);
+
+    {
+        auto x = a.find("/xyz/openbmc_project/inventory/system/PS0");
+        EXPECT_NE(x, a.end());
+
+        auto& endpoints = x->second;
+        EXPECT_EQ(endpoints.size(), 2);
+
+        {
+            auto& types = std::get<0>(endpoints[0]);
+            EXPECT_EQ(std::get<0>(types), "sensors");
+            EXPECT_EQ(std::get<1>(types), "inventory");
+
+            auto& paths = std::get<1>(endpoints[0]);
+            EXPECT_EQ(paths.size(), 4);
+        }
+        {
+            auto& types = std::get<0>(endpoints[1]);
+            EXPECT_EQ(std::get<0>(types), "fans");
+            EXPECT_EQ(std::get<1>(types), "inventory");
+
+            auto& paths = std::get<1>(endpoints[1]);
+            EXPECT_EQ(paths.size(), 1);
+        }
+    }
+    {
+        auto x = a.find("/xyz/openbmc_project/inventory/system/fan42");
+        EXPECT_NE(x, a.end());
+
+        auto& endpoints = x->second;
+        EXPECT_EQ(endpoints.size(), 2);
+
+        {
+            auto& types = std::get<0>(endpoints[0]);
+            EXPECT_EQ(std::get<0>(types), "sensors");
+            EXPECT_EQ(std::get<1>(types), "inventory");
+
+            auto& paths = std::get<1>(endpoints[0]);
+            EXPECT_EQ(paths.size(), 1);
+        }
+        {
+            auto& types = std::get<0>(endpoints[1]);
+            EXPECT_EQ(std::get<0>(types), "led");
+            EXPECT_EQ(std::get<1>(types), "inventory");
+
+            auto& paths = std::get<1>(endpoints[1]);
+            EXPECT_EQ(paths.size(), 1);
+        }
+    }
+}
+
+TEST_F(AssocsTest, TEST_BAD_JSON0)
+{
+    auto path = writeFile(badJson0);
+
+    try
+    {
+        Manager m(bus, path);
+
+        EXPECT_TRUE(false);
+    }
+    catch (std::exception& e)
+    {
+    }
+}
+
+TEST_F(AssocsTest, TEST_BAD_JSON1)
+{
+    auto path = writeFile(badJson1);
+
+    try
+    {
+        Manager m(bus, path);
+
+        EXPECT_TRUE(false);
+    }
+    catch (std::exception& e)
+    {
+    }
+}
+
+TEST_F(AssocsTest, TEST_BAD_JSON2)
+{
+    auto path = writeFile(badJson2);
+
+    try
+    {
+        Manager m(bus, path);
+
+        EXPECT_TRUE(false);
+    }
+    catch (std::exception& e)
+    {
+    }
+}
+
+TEST_F(AssocsTest, TEST_BAD_JSON3)
+{
+    auto path = writeFile(badJson3);
+
+    try
+    {
+        Manager m(bus, path);
+
+        EXPECT_TRUE(false);
+    }
+    catch (std::exception& e)
+    {
+    }
+}