buffet: Add parsing of command instances from JSON

CommandInstance class can be created from a JSON object
with proper command and parameter value validation against
command definition schema.

BUG=chromium:396713
TEST=USE=buffet P2_TEST_FILTER="buffet::*" FEATURES=test emerge-link platform2

Change-Id: Iba4c807225552f6a9d8b33a0aa1fc451e75753a4
Reviewed-on: https://chromium-review.googlesource.com/211338
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/commands/command_instance_unittest.cc b/buffet/commands/command_instance_unittest.cc
index 0577004..cb639b5 100644
--- a/buffet/commands/command_instance_unittest.cc
+++ b/buffet/commands/command_instance_unittest.cc
@@ -4,8 +4,62 @@
 
 #include <gtest/gtest.h>
 
+#include "buffet/commands/command_dictionary.h"
 #include "buffet/commands/command_instance.h"
 #include "buffet/commands/prop_types.h"
+#include "buffet/commands/unittest_utils.h"
+
+using buffet::unittests::CreateDictionaryValue;
+using buffet::unittests::CreateValue;
+
+namespace {
+
+class CommandInstanceTest : public ::testing::Test {
+ protected:
+  virtual void SetUp() override {
+    auto json = CreateDictionaryValue(R"({
+      'base': {
+        'reboot': {
+          'parameters': {}
+        }
+      },
+      'robot': {
+        'jump': {
+          'parameters': {
+            'height': {
+              'type': 'integer',
+              'minimum': 0,
+              'maximum': 100
+            },
+            '_jumpType': {
+              'type': 'string',
+              'enum': ['_withAirFlip', '_withSpin', '_withKick']
+            }
+          }
+        },
+        'speak': {
+          'parameters': {
+            'phrase': {
+              'type': 'string',
+              'enum': ['beamMeUpScotty', 'iDontDigOnSwine',
+                       'iPityDaFool', 'dangerWillRobinson']
+            },
+            'volume': {
+              'type': 'integer',
+              'minimum': 0,
+              'maximum': 10
+            }
+          }
+        }
+      }
+    })");
+    CHECK(dict_.LoadCommands(*json, "robotd", nullptr, nullptr))
+        << "Failed to parse test command dictionary";
+  }
+  buffet::CommandDictionary dict_;
+};
+
+}  // anonymous namespace
 
 TEST(CommandInstance, Test) {
   buffet::native_types::Object params;
@@ -23,3 +77,92 @@
   EXPECT_EQ(100, instance.FindParameter("volume")->GetInt()->GetValue());
   EXPECT_EQ(nullptr, instance.FindParameter("blah").get());
 }
+
+TEST_F(CommandInstanceTest, FromJson) {
+  auto json = CreateDictionaryValue(R"({
+    'name': 'robot.jump',
+    'parameters': {
+      'height': 53,
+      '_jumpType': '_withKick'
+    }
+  })");
+  auto instance = buffet::CommandInstance::FromJson(json.get(), dict_, nullptr);
+  EXPECT_EQ("robot.jump", instance->GetName());
+  EXPECT_EQ("robotd", instance->GetCategory());
+  EXPECT_EQ(53, instance->FindParameter("height")->GetInt()->GetValue());
+  EXPECT_EQ("_withKick",
+            instance->FindParameter("_jumpType")->GetString()->GetValue());
+}
+
+TEST_F(CommandInstanceTest, FromJson_ParamsOmitted) {
+  auto json = CreateDictionaryValue("{'name': 'base.reboot'}");
+  auto instance = buffet::CommandInstance::FromJson(json.get(), dict_, nullptr);
+  EXPECT_EQ("base.reboot", instance->GetName());
+  EXPECT_EQ("robotd", instance->GetCategory());
+  EXPECT_TRUE(instance->GetParameters().empty());
+}
+
+TEST_F(CommandInstanceTest, FromJson_NotObject) {
+  auto json = CreateValue("'string'");
+  buffet::ErrorPtr error;
+  auto instance = buffet::CommandInstance::FromJson(json.get(), dict_, &error);
+  EXPECT_EQ(nullptr, instance.get());
+  EXPECT_EQ("json_object_expected", error->GetCode());
+  EXPECT_EQ("Command instance is not a JSON object", error->GetMessage());
+}
+
+TEST_F(CommandInstanceTest, FromJson_NameMissing) {
+  auto json = CreateDictionaryValue("{'param': 'value'}");
+  buffet::ErrorPtr error;
+  auto instance = buffet::CommandInstance::FromJson(json.get(), dict_, &error);
+  EXPECT_EQ(nullptr, instance.get());
+  EXPECT_EQ("parameter_missing", error->GetCode());
+  EXPECT_EQ("Command name is missing", error->GetMessage());
+}
+
+TEST_F(CommandInstanceTest, FromJson_UnknownCommand) {
+  auto json = CreateDictionaryValue("{'name': 'robot.scream'}");
+  buffet::ErrorPtr error;
+  auto instance = buffet::CommandInstance::FromJson(json.get(), dict_, &error);
+  EXPECT_EQ(nullptr, instance.get());
+  EXPECT_EQ("invalid_command_name", error->GetCode());
+  EXPECT_EQ("Unknown command received: robot.scream", error->GetMessage());
+}
+
+TEST_F(CommandInstanceTest, FromJson_ParamsNotObject) {
+  auto json = CreateDictionaryValue(R"({
+    'name': 'robot.speak',
+    'parameters': 'hello'
+  })");
+  buffet::ErrorPtr error;
+  auto instance = buffet::CommandInstance::FromJson(json.get(), dict_, &error);
+  EXPECT_EQ(nullptr, instance.get());
+  auto inner = error->GetInnerError();
+  EXPECT_EQ("json_object_expected", inner->GetCode());
+  EXPECT_EQ("Property 'parameters' must be a JSON object", inner->GetMessage());
+  EXPECT_EQ("command_failed", error->GetCode());
+  EXPECT_EQ("Failed to validate command 'robot.speak'", error->GetMessage());
+}
+
+TEST_F(CommandInstanceTest, FromJson_ParamError) {
+  auto json = CreateDictionaryValue(R"({
+    'name': 'robot.speak',
+    'parameters': {
+      'phrase': 'iPityDaFool',
+      'volume': 20
+    }
+  })");
+  buffet::ErrorPtr error;
+  auto instance = buffet::CommandInstance::FromJson(json.get(), dict_, &error);
+  EXPECT_EQ(nullptr, instance.get());
+  auto first = error->GetFirstError();
+  EXPECT_EQ("out_of_range", first->GetCode());
+  EXPECT_EQ("Value 20 is out of range. It must not be greater than 10",
+            first->GetMessage());
+  auto inner = error->GetInnerError();
+  EXPECT_EQ("invalid_parameter_value", inner->GetCode());
+  EXPECT_EQ("Invalid parameter value for property 'volume'",
+            inner->GetMessage());
+  EXPECT_EQ("command_failed", error->GetCode());
+  EXPECT_EQ("Failed to validate command 'robot.speak'", error->GetMessage());
+}