add json verification for configurations

Add json verificiation for configurations.  A configuration is lightly
validated.

Change-Id: I42361daf6ad21d3480e92c3808f5fc8ab8318e0b
Signed-off-by: Patrick Venture <venture@google.com>
diff --git a/Makefile.am b/Makefile.am
index cce4f4c..c227281 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -78,6 +78,7 @@
 	pid/util.cpp \
 	pid/pidthread.cpp \
 	threads/busthread.cpp \
+	build/buildjson.cpp \
 	experiments/drive.cpp \
 	$(BUILT_SOURCES)
 
diff --git a/build/buildjson.cpp b/build/buildjson.cpp
new file mode 100644
index 0000000..a5ae502
--- /dev/null
+++ b/build/buildjson.cpp
@@ -0,0 +1,64 @@
+#include "build/buildjson.hpp"
+
+#include "errors/exception.hpp"
+
+#include <fstream>
+#include <nlohmann/json.hpp>
+
+using json = nlohmann::json;
+
+void validateJson(const json& data)
+{
+    if (data.count("sensors") != 1)
+    {
+        throw ConfigurationException(
+            "KeyError: 'sensors' not found (or found repeatedly)");
+    }
+
+    if (data["sensors"].size() == 0)
+    {
+        throw ConfigurationException(
+            "Invalid Configuration: At least one sensor required");
+    }
+
+    if (data.count("zones") != 1)
+    {
+        throw ConfigurationException(
+            "KeyError: 'zones' not found (or found repeatedly)");
+    }
+
+    for (const auto& zone : data["zones"])
+    {
+        if (zone.count("pids") != 1)
+        {
+            throw ConfigurationException(
+                "KeyError: should only have one 'pids' key per zone.");
+        }
+
+        if (zone["pids"].size() == 0)
+        {
+            throw ConfigurationException(
+                "Invalid Configuration: must be at least one pid per zone.");
+        }
+    }
+}
+
+json parseValidateJson(const std::string& path)
+{
+    std::ifstream jsonFile(path);
+    if (!jsonFile.is_open())
+    {
+        throw ConfigurationException("Unable to open json file");
+    }
+
+    auto data = json::parse(jsonFile, nullptr, false);
+    if (data.is_discarded())
+    {
+        throw ConfigurationException("Invalid json - parse failed");
+    }
+
+    /* Check the data. */
+    validateJson(data);
+
+    return data;
+}
diff --git a/build/buildjson.hpp b/build/buildjson.hpp
new file mode 100644
index 0000000..afc90df
--- /dev/null
+++ b/build/buildjson.hpp
@@ -0,0 +1,27 @@
+#pragma once
+
+#include <nlohmann/json.hpp>
+#include <string>
+
+using json = nlohmann::json;
+
+/**
+ * Given json data, validate the minimum.
+ * The json data must be valid, and must contain two keys:
+ * sensors, and zones.
+ *
+ * @param[in] data - the json data.
+ * @return nothing - throws exceptions on invalid bits.
+ */
+void validateJson(const json& data);
+
+/**
+ * Given a json configuration file, parse it.
+ *
+ * There must be at least one sensor, and one zone.
+ * That one zone must contain at least one PID.
+ *
+ * @param[in] path - path to the configuration
+ * @return the json data.
+ */
+json parseValidateJson(const std::string& path);
diff --git a/test/Makefile.am b/test/Makefile.am
index 5375eb4..04cf2df 100644
--- a/test/Makefile.am
+++ b/test/Makefile.am
@@ -18,7 +18,7 @@
  pid_thermalcontroller_unittest pid_fancontroller_unittest \
  pid_stepwisecontroller_unittest \
  dbus_passive_unittest dbus_active_unittest \
- sensors_json_unittest pid_json_unittest
+ sensors_json_unittest pid_json_unittest json_parse_unittest
 TESTS = $(check_PROGRAMS)
 
 # Until libconfig is mocked out or replaced, include it.
@@ -65,3 +65,6 @@
 
 pid_json_unittest_SOURCES = pid_json_unittest.cpp
 pid_json_unittest_LDADD = $(top_builddir)/pid/buildjson.o
+
+json_parse_unittest_SOURCES = json_parse_unittest.cpp
+json_parse_unittest_LDADD = $(top_builddir)/build/buildjson.o
diff --git a/test/json_parse_unittest.cpp b/test/json_parse_unittest.cpp
new file mode 100644
index 0000000..7d3d449
--- /dev/null
+++ b/test/json_parse_unittest.cpp
@@ -0,0 +1,154 @@
+#include "build/buildjson.hpp"
+#include "errors/exception.hpp"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+TEST(ConfigurationVerificationTest, VerifyHappy)
+{
+    /* Verify a happy configuration throws no exceptions. */
+    auto j2 = R"(
+      {
+        "sensors": [{
+          "name": "fan1",
+          "type": "fan",
+          "readPath": "/xyz/openbmc_project/sensors/fan_tach/fan1"
+        }],
+        "zones": [{
+          "id": 1,
+          "minThermalRpm": 3000.0,
+          "failsafePercent": 75.0,
+          "pids": [{
+            "name": "fan1-5",
+            "type": "fan",
+            "inputs": ["fan1", "fan5"],
+            "setpoint": 90.0,
+            "pid": {
+              "samplePeriod": 0.1,
+              "proportionalCoeff": 0.0,
+              "integralCoeff": 0.0,
+              "feedFwdOffOffsetCoeff": 0.0,
+              "feedFwdGainCoeff": 0.010,
+              "integralLimit_min": 0.0,
+              "integralLimit_max": 0.0,
+              "outLim_min": 30.0,
+              "outLim_max": 100.0,
+              "slewNeg": 0.0,
+              "slewPos": 0.0
+            }
+          }]
+        }]
+      }
+    )"_json;
+
+    validateJson(j2);
+}
+
+TEST(ConfigurationVerificationTest, VerifyNoSensorKey)
+{
+    /* Verify the sensors key must be present. */
+    auto j2 = R"(
+      {
+        "zones": [{
+          "id": 1,
+          "minThermalRpm": 3000.0,
+          "failsafePercent": 75.0,
+          "pids": [{
+            "name": "fan1-5",
+            "type": "fan",
+            "inputs": ["fan1", "fan5"],
+            "setpoint": 90.0,
+            "pid": {
+              "samplePeriod": 0.1,
+              "proportionalCoeff": 0.0,
+              "integralCoeff": 0.0,
+              "feedFwdOffOffsetCoeff": 0.0,
+              "feedFwdGainCoeff": 0.010,
+              "integralLimit_min": 0.0,
+              "integralLimit_max": 0.0,
+              "outLim_min": 30.0,
+              "outLim_max": 100.0,
+              "slewNeg": 0.0,
+              "slewPos": 0.0
+            }
+          }]
+        }]
+      }
+    )"_json;
+
+    EXPECT_THROW(validateJson(j2), ConfigurationException);
+}
+
+TEST(ConfigurationVerificationTest, VerifyNoZoneKey)
+{
+    /* Verify the zones key must be present. */
+    auto j2 = R"(
+      {
+        "sensors": [{
+          "name": "fan1",
+          "type": "fan",
+          "readPath": "/xyz/openbmc_project/sensors/fan_tach/fan1"
+        }]
+      }
+    )"_json;
+
+    EXPECT_THROW(validateJson(j2), ConfigurationException);
+}
+
+TEST(ConfigurationVerificationTest, VerifyNoSensor)
+{
+    /* Verify that there needs to be at least one sensor in the sensors key. */
+    auto j2 = R"(
+      {
+        "sensors": [],
+        "zones": [{
+          "id": 1,
+          "minThermalRpm": 3000.0,
+          "failsafePercent": 75.0,
+          "pids": [{
+            "name": "fan1-5",
+            "type": "fan",
+            "inputs": ["fan1", "fan5"],
+            "setpoint": 90.0,
+            "pid": {
+              "samplePeriod": 0.1,
+              "proportionalCoeff": 0.0,
+              "integralCoeff": 0.0,
+              "feedFwdOffOffsetCoeff": 0.0,
+              "feedFwdGainCoeff": 0.010,
+              "integralLimit_min": 0.0,
+              "integralLimit_max": 0.0,
+              "outLim_min": 30.0,
+              "outLim_max": 100.0,
+              "slewNeg": 0.0,
+              "slewPos": 0.0
+            }
+          }]
+        }]
+      }
+    )"_json;
+
+    EXPECT_THROW(validateJson(j2), ConfigurationException);
+}
+
+TEST(ConfigurationVerificationTest, VerifyNoPidInZone)
+{
+    /* Verify that there needs to be at least one PID in the zone. */
+    auto j2 = R"(
+      {
+        "sensors": [{
+          "name": "fan1",
+          "type": "fan",
+          "readPath": "/xyz/openbmc_project/sensors/fan_tach/fan1"
+        }],
+        "zones": [{
+          "id": 1,
+          "minThermalRpm": 3000.0,
+          "failsafePercent": 75.0,
+          "pids": []
+        }]
+      }
+    )"_json;
+
+    EXPECT_THROW(validateJson(j2), ConfigurationException);
+}