Add a class to handle ExternalStorer file writes.

Signed-off-by: Kasun Athukorala <kasunath@google.com>
Change-Id: Ic1092a6a1da3375f595774018abfecd08a3cb7d8
diff --git a/include/rde/external_storer_file.hpp b/include/rde/external_storer_file.hpp
new file mode 100644
index 0000000..3d12dc9
--- /dev/null
+++ b/include/rde/external_storer_file.hpp
@@ -0,0 +1,139 @@
+#pragma once
+
+#include "external_storer_interface.hpp"
+#include "nlohmann/json.hpp"
+
+#include <boost/uuid/uuid_generators.hpp>
+
+#include <filesystem>
+#include <string>
+
+namespace bios_bmc_smm_error_logger
+{
+namespace rde
+{
+
+/**
+ * @brief Simple Base class for writing JSON data to files.
+ *
+ * This class allows us to unit test the ExternalStorerFileInterface
+ * functionality.
+ */
+class FileHandlerInterface
+{
+  public:
+    virtual ~FileHandlerInterface() = default;
+
+    /**
+     * @brief Create a folder at the provided path.
+     *
+     * @param[in] folderPath - folder path.
+     * @return true if successful.
+     */
+    virtual bool createFolder(const std::string& folderPath) const = 0;
+
+    /**
+     * @brief Create an index.json and write the JSON content to it.
+     *
+     * If the file already exists, this will overwrite it.
+     *
+     * @param[in] folderPath - path of the file without including the file name.
+     * @param[in] jsonPdr - PDR in nlohmann::json format.
+     * @return true if successful.
+     */
+    virtual bool createFile(const std::string& folderPath,
+                            const nlohmann::json& jsonPdr) const = 0;
+};
+
+/**
+ * @brief Class for handling folder and file creation for ExternalStorer.
+ */
+class ExternalStorerFileWriter : public FileHandlerInterface
+{
+  public:
+    bool createFolder(const std::string& folderPath) const override;
+    bool createFile(const std::string& folderPath,
+                    const nlohmann::json& jsonPdr) const override;
+};
+
+/**
+ * @brief Categories for different redfish JSON strings.
+ */
+enum class JsonPdrType
+{
+    logEntry,
+    logService,
+    other
+};
+
+/**
+ * @brief Class for handling ExternalStorer file operations.
+ */
+class ExternalStorerFileInterface : public ExternalStorerInterface
+{
+  public:
+    /**
+     * @brief Constructor for the ExternalStorerFileInterface.
+     *
+     * @param[in] rootPath - root path for creating redfish folders.
+     * Eg: "/run/bmcweb"
+     * @param[in] fileHandler - an ExternalStorerFileWriter object. This class
+     * will take the ownership of this object.
+     */
+    ExternalStorerFileInterface(
+        std::string_view rootPath,
+        std::unique_ptr<FileHandlerInterface> fileHandler);
+
+    bool publishJson(std::string_view jsonStr) override;
+
+  private:
+    std::string rootPath;
+    std::unique_ptr<FileHandlerInterface> fileHandler;
+    std::string logServiceId;
+    boost::uuids::random_generator randomGen;
+
+    /**
+     * @brief Get the type of the received PDR.
+     *
+     * @param[in] jsonSchema - PDR in nlohmann::json format.
+     * @return JsonPdrType of the PDR.
+     */
+    JsonPdrType getSchemaType(const nlohmann::json& jsonSchema) const;
+
+    /**
+     * @brief Process a LogEntry type PDR.
+     *
+     * @param[in] logEntry - PDR in nlohmann::json format.
+     * @return true if successful.
+     */
+    bool processLogEntry(nlohmann::json& logEntry);
+
+    /**
+     * @brief Process a LogService type PDR.
+     *
+     * @param[in] logService - PDR in nlohmann::json format.
+     * @return true if successful.
+     */
+    bool processLogService(const nlohmann::json& logService);
+
+    /**
+     * @brief Process PDRs that doesn't have a specific category.
+     *
+     * @param[in] jsonPdr - PDR in nlohmann::json format.
+     * @return true if successful.
+     */
+    bool processOtherTypes(const nlohmann::json& jsonPdr) const;
+
+    /**
+     * @brief Create the needed folders and the index.json.
+     *
+     * @param subPath - path within the root folder.
+     * @param jsonPdr - PDR in nlohmann::json format.
+     * @return true if successful.
+     */
+    bool createFile(const std::string& subPath,
+                    const nlohmann::json& jsonPdr) const;
+};
+
+} // namespace rde
+} // namespace bios_bmc_smm_error_logger
diff --git a/include/rde/external_storer_interface.hpp b/include/rde/external_storer_interface.hpp
new file mode 100644
index 0000000..05a8616
--- /dev/null
+++ b/include/rde/external_storer_interface.hpp
@@ -0,0 +1,28 @@
+#pragma once
+
+#include <string_view>
+
+namespace bios_bmc_smm_error_logger
+{
+namespace rde
+{
+
+/**
+ * @brief Base class for publishing data to ExternalStorer.
+ */
+class ExternalStorerInterface
+{
+  public:
+    virtual ~ExternalStorerInterface() = default;
+
+    /**
+     * @brief Publish JSON string to ExternalStorer.
+     *
+     * @param[in] jsonStr - a valid JSON string.
+     * @return true if successful.
+     */
+    virtual bool publishJson(std::string_view jsonStr) = 0;
+};
+
+} // namespace rde
+} // namespace bios_bmc_smm_error_logger
diff --git a/src/rde/external_storer_file.cpp b/src/rde/external_storer_file.cpp
new file mode 100644
index 0000000..0768134
--- /dev/null
+++ b/src/rde/external_storer_file.cpp
@@ -0,0 +1,180 @@
+#include "rde/external_storer_file.hpp"
+
+#include <fmt/format.h>
+
+#include <boost/uuid/uuid.hpp>
+#include <boost/uuid/uuid_io.hpp>
+
+#include <fstream>
+#include <string_view>
+
+namespace bios_bmc_smm_error_logger
+{
+namespace rde
+{
+
+bool ExternalStorerFileWriter::createFolder(const std::string& folderPath) const
+{
+    std::filesystem::path path(folderPath);
+    if (!std::filesystem::is_directory(path))
+    {
+        if (!std::filesystem::create_directories(path))
+        {
+            fmt::print(stderr, "Failed to create a folder at {}\n", folderPath);
+            return false;
+        }
+    }
+    return true;
+}
+
+bool ExternalStorerFileWriter::createFile(const std::string& folderPath,
+                                          const nlohmann::json& jsonPdr) const
+{
+    if (!createFolder(folderPath))
+    {
+        return false;
+    }
+    std::filesystem::path path(folderPath);
+    path /= "index.json";
+    // If the file already exist, overwrite it.
+    std::ofstream output(path);
+    output << jsonPdr;
+    output.close();
+    return true;
+}
+
+ExternalStorerFileInterface::ExternalStorerFileInterface(
+    std::string_view rootPath,
+    std::unique_ptr<FileHandlerInterface> fileHandler) :
+    rootPath(rootPath),
+    fileHandler(std::move(fileHandler)), logServiceId("")
+{}
+
+bool ExternalStorerFileInterface::publishJson(std::string_view jsonStr)
+{
+    nlohmann::json jsonDecoded;
+    try
+    {
+        jsonDecoded = nlohmann::json::parse(jsonStr);
+    }
+    catch (nlohmann::json::parse_error& e)
+    {
+        fmt::print(stderr, "JSON parse error: \n{}\n", e.what());
+        return false;
+    }
+
+    // We need to know the type to determine how to process the decoded JSON
+    // output.
+    if (!jsonDecoded.contains("@odata.type"))
+    {
+        fmt::print(stderr, "@odata.type field doesn't exist in:\n {}\n",
+                   jsonDecoded.dump(4));
+        return false;
+    }
+
+    auto schemaType = getSchemaType(jsonDecoded);
+    if (schemaType == JsonPdrType::logEntry)
+    {
+        return processLogEntry(jsonDecoded);
+    }
+    if (schemaType == JsonPdrType::logService)
+    {
+        return processLogService(jsonDecoded);
+    }
+    return processOtherTypes(jsonDecoded);
+}
+
+JsonPdrType ExternalStorerFileInterface::getSchemaType(
+    const nlohmann::json& jsonSchema) const
+{
+    auto logEntryFound =
+        std::string(jsonSchema["@odata.type"]).find("LogEntry");
+    if (logEntryFound != std::string::npos)
+    {
+        return JsonPdrType::logEntry;
+    }
+
+    auto logServiceFound =
+        std::string(jsonSchema["@odata.type"]).find("LogService");
+    if (logServiceFound != std::string::npos)
+    {
+        return JsonPdrType::logService;
+    }
+
+    return JsonPdrType::other;
+}
+
+bool ExternalStorerFileInterface::processLogEntry(nlohmann::json& logEntry)
+{
+    // TODO: Add policies for LogEntry retention.
+    // https://github.com/openbmc/bios-bmc-smm-error-logger/issues/1.
+    if (logServiceId.empty())
+    {
+        fmt::print(stderr, "First need a LogService PDR with a new UUID.\n");
+        return false;
+    }
+
+    std::string id = boost::uuids::to_string(randomGen());
+    std::string path = "/redfish/v1/Systems/system/LogServices/" +
+                       logServiceId + "/Entries/" + id;
+
+    // Populate the "Id" with the UUID we generated.
+    logEntry["Id"] = id;
+    // Remove the @odata.id from the JSON since ExternalStorer will fill it for
+    // a client.
+    logEntry.erase("@odata.id");
+
+    return createFile(path, logEntry);
+}
+
+bool ExternalStorerFileInterface::processLogService(
+    const nlohmann::json& logService)
+{
+    if (!logService.contains("@odata.id"))
+    {
+        fmt::print(stderr, "@odata.id field doesn't exist in:\n {}\n",
+                   logService.dump(4));
+        return false;
+    }
+
+    if (!logService.contains("Id"))
+    {
+        fmt::print(stderr, "Id field doesn't exist in:\n {}\n",
+                   logService.dump(4));
+        return false;
+    }
+
+    logServiceId = logService["Id"].get<std::string>();
+
+    if (!createFile(logService["@odata.id"].get<std::string>(), logService))
+    {
+        fmt::print(stderr, "Failed to create LogService index file for:\n{}\n",
+                   logService.dump(4));
+        return false;
+    }
+    // ExternalStorer needs a .../Entries/index.json file with no data.
+    nlohmann::json jEmpty = "{}"_json;
+    return createFile(logService["@odata.id"].get<std::string>() + "/Entries",
+                      jEmpty);
+}
+
+bool ExternalStorerFileInterface::processOtherTypes(
+    const nlohmann::json& jsonPdr) const
+{
+    if (!jsonPdr.contains("@odata.id"))
+    {
+        fmt::print(stderr, "@odata.id field doesn't exist in:\n {}\n",
+                   jsonPdr.dump(4));
+        return false;
+    }
+    return createFile(jsonPdr["@odata.id"].get<std::string>(), jsonPdr);
+}
+
+bool ExternalStorerFileInterface::createFile(
+    const std::string& subPath, const nlohmann::json& jsonPdr) const
+{
+    return fileHandler->createFile(rootPath + subPath, jsonPdr);
+}
+
+} // namespace rde
+} // namespace bios_bmc_smm_error_logger
diff --git a/src/rde/meson.build b/src/rde/meson.build
index 4e6220f..482e9ff 100644
--- a/src/rde/meson.build
+++ b/src/rde/meson.build
@@ -1,6 +1,7 @@
 rde_lib = static_library(
   'rde',
   'rde_dictionary_manager.cpp',
+  'external_storer_file.cpp',
   include_directories : rde_inc,
   implicit_include_directories: false)
 
diff --git a/test/external_storer_file_test.cpp b/test/external_storer_file_test.cpp
new file mode 100644
index 0000000..fc550ee
--- /dev/null
+++ b/test/external_storer_file_test.cpp
@@ -0,0 +1,189 @@
+#include "rde/external_storer_file.hpp"
+
+#include <string_view>
+
+#include <gmock/gmock-matchers.h>
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+namespace bios_bmc_smm_error_logger
+{
+namespace rde
+{
+
+using ::testing::_;
+using ::testing::DoAll;
+using ::testing::Return;
+using ::testing::SaveArg;
+
+class MockFileWriter : public FileHandlerInterface
+{
+  public:
+    MOCK_METHOD(bool, createFolder, (const std::string& path),
+                (const, override));
+    MOCK_METHOD(bool, createFile,
+                (const std::string& path, const nlohmann::json& jsonPdr),
+                (const, override));
+};
+
+class ExternalStorerFileTest : public ::testing::Test
+{
+  public:
+    ExternalStorerFileTest() :
+        mockFileWriter(std::make_unique<MockFileWriter>())
+    {
+        mockFileWriterPtr = dynamic_cast<MockFileWriter*>(mockFileWriter.get());
+        exStorer = std::make_unique<ExternalStorerFileInterface>(
+            rootPath, std::move(mockFileWriter));
+    }
+
+  protected:
+    std::unique_ptr<FileHandlerInterface> mockFileWriter;
+    std::unique_ptr<ExternalStorerFileInterface> exStorer;
+    MockFileWriter* mockFileWriterPtr;
+    const std::string rootPath = "/some/path";
+};
+
+TEST_F(ExternalStorerFileTest, InvalidJsonTest)
+{
+    // Try an invalid JSON.
+    std::string jsonStr = "Invalid JSON";
+    EXPECT_THAT(exStorer->publishJson(jsonStr), false);
+}
+
+TEST_F(ExternalStorerFileTest, NoOdataTypeFailureTest)
+{
+    // Try a JSON without @odata.type.
+    std::string jsonStr = R"(
+      {
+        "@odata.id": "/redfish/v1/Systems/system/Memory/dimm0/MemoryMetrics",
+        "Id":"Metrics"
+      }
+    )";
+    EXPECT_THAT(exStorer->publishJson(jsonStr), false);
+}
+
+TEST_F(ExternalStorerFileTest, LogServiceNoOdataIdTest)
+{
+    // Try a LogService without @odata.id.
+    std::string jsonStr = R"(
+      {
+        "@odata.type": "#LogService.v1_1_0.LogService","Id":"6F7-C1A7C"
+      }
+    )";
+    EXPECT_THAT(exStorer->publishJson(jsonStr), false);
+}
+
+TEST_F(ExternalStorerFileTest, LogServiceNoIdTest)
+{
+    // Try a LogService without Id.
+    std::string jsonStr = R"(
+      {
+        "@odata.id": "/redfish/v1/Systems/system/LogServices/6F7-C1A7C",
+        "@odata.type": "#LogService.v1_1_0.LogService"
+      }
+    )";
+    EXPECT_THAT(exStorer->publishJson(jsonStr), false);
+}
+
+TEST_F(ExternalStorerFileTest, LogServiceTest)
+{
+    // A valid LogService test.
+    std::string jsonStr = R"(
+      {
+        "@odata.id": "/redfish/v1/Systems/system/LogServices/6F7-C1A7C",
+        "@odata.type": "#LogService.v1_1_0.LogService","Id":"6F7-C1A7C"
+        }
+      )";
+    std::string exServiceFolder =
+        "/some/path/redfish/v1/Systems/system/LogServices/6F7-C1A7C";
+    std::string exEntriesFolder =
+        "/some/path/redfish/v1/Systems/system/LogServices/6F7-C1A7C/Entries";
+    nlohmann::json exEntriesJson = "{}"_json;
+    nlohmann::json exServiceJson = nlohmann::json::parse(jsonStr);
+    EXPECT_CALL(*mockFileWriterPtr, createFile(exServiceFolder, exServiceJson))
+        .WillOnce(Return(true));
+    EXPECT_CALL(*mockFileWriterPtr, createFile(exEntriesFolder, exEntriesJson))
+        .WillOnce(Return(true));
+    EXPECT_THAT(exStorer->publishJson(jsonStr), true);
+}
+
+TEST_F(ExternalStorerFileTest, LogEntryWithoutLogServiceTest)
+{
+    // Try a LogEntry without sending a LogService first.
+    std::string jsonLogEntry = R"(
+      {
+        "@odata.type": "#LogEntry.v1_13_0.LogEntry"
+      }
+    )";
+    EXPECT_THAT(exStorer->publishJson(jsonLogEntry), false);
+}
+
+TEST_F(ExternalStorerFileTest, LogEntryTest)
+{
+    // Before sending a LogEntry, first we need to push a LogService.
+    std::string jsonLogSerivce = R"(
+      {
+        "@odata.id": "/redfish/v1/Systems/system/LogServices/6F7-C1A7C",
+        "@odata.type": "#LogService.v1_1_0.LogService","Id":"6F7-C1A7C"
+      }
+    )";
+    std::string exServiceFolder =
+        "/some/path/redfish/v1/Systems/system/LogServices/6F7-C1A7C";
+    std::string exEntriesFolder =
+        "/some/path/redfish/v1/Systems/system/LogServices/6F7-C1A7C/Entries";
+    nlohmann::json exEntriesJson = "{}"_json;
+    nlohmann::json exServiceJson = nlohmann::json::parse(jsonLogSerivce);
+    EXPECT_CALL(*mockFileWriterPtr, createFile(exServiceFolder, exServiceJson))
+        .WillOnce(Return(true));
+    EXPECT_CALL(*mockFileWriterPtr, createFile(exEntriesFolder, exEntriesJson))
+        .WillOnce(Return(true));
+    EXPECT_THAT(exStorer->publishJson(jsonLogSerivce), true);
+
+    // Now send a LogEntry
+    std::string jsonLogEntry = R"(
+      {
+        "@odata.id": "/some/odata/id",
+        "@odata.type": "#LogEntry.v1_13_0.LogEntry"
+      }
+    )";
+    nlohmann::json logEntryOut;
+    EXPECT_CALL(*mockFileWriterPtr, createFile(_, _))
+        .WillOnce(DoAll(SaveArg<1>(&logEntryOut), Return(true)));
+    EXPECT_THAT(exStorer->publishJson(jsonLogEntry), true);
+    EXPECT_NE(logEntryOut["Id"], nullptr);
+    EXPECT_EQ(logEntryOut["@odata.id"], nullptr);
+}
+
+TEST_F(ExternalStorerFileTest, OtherSchemaNoOdataIdTest)
+{
+    // Try a another PDRs without @odata.id.
+    std::string jsonStr = R"(
+      {
+        "@odata.type": "#MemoryMetrics.v1_4_1.MemoryMetrics",
+        "Id":"Metrics"
+      }
+    )";
+    EXPECT_THAT(exStorer->publishJson(jsonStr), false);
+}
+
+TEST_F(ExternalStorerFileTest, OtherSchemaTypeTest)
+{
+    // A valid MemoryMetrics PDR.
+    std::string jsonStr = R"(
+      {
+        "@odata.id": "/redfish/v1/Systems/system/Memory/dimm0/MemoryMetrics",
+        "@odata.type": "#MemoryMetrics.v1_4_1.MemoryMetrics",
+        "Id": "Metrics"
+      }
+    )";
+    std::string exFolder =
+        "/some/path/redfish/v1/Systems/system/Memory/dimm0/MemoryMetrics";
+    nlohmann::json exJson = nlohmann::json::parse(jsonStr);
+    EXPECT_CALL(*mockFileWriterPtr, createFile(exFolder, exJson))
+        .WillOnce(Return(true));
+    EXPECT_THAT(exStorer->publishJson(jsonStr), true);
+}
+
+} // namespace rde
+} // namespace bios_bmc_smm_error_logger
diff --git a/test/meson.build b/test/meson.build
index 6f72d9f..01a92a2 100644
--- a/test/meson.build
+++ b/test/meson.build
@@ -26,6 +26,7 @@
   'pci_handler',
   'rde_dictionary_manager',
   'buffer',
+  'external_storer_file',
 ]
 foreach t : gtests
   test(t, executable(t.underscorify(), t + '_test.cpp',