buffet: Add command definition support for GCD command manager

Rudimentary framework of classes to represent GCD command manager
and list of registered device command definitions/schemas.

BUG=chromium:374861
TEST=USE=buffet P2_TEST_FILTER="buffet::*" FEATURES=test emerge-link platform

Change-Id: I3cd3d776879e8bd506aecd20df5cd89c65247d35
Reviewed-on: https://chromium-review.googlesource.com/208464
Tested-by: Alex Vakulenko <avakulenko@chromium.org>
Reviewed-by: Christopher Wiley <wiley@chromium.org>
Reviewed-by: Alex Vakulenko <avakulenko@chromium.org>
Commit-Queue: Alex Vakulenko <avakulenko@chromium.org>
diff --git a/buffet/buffet.gyp b/buffet/buffet.gyp
index 0174e5b..2197989 100644
--- a/buffet/buffet.gyp
+++ b/buffet/buffet.gyp
@@ -25,6 +25,9 @@
       'sources': [
         'any.cc',
         'async_event_sequencer.cc',
+        'commands/command_definition.cc',
+        'commands/command_dictionary.cc',
+        'commands/command_manager.cc',
         'commands/object_schema.cc',
         'commands/prop_constraints.cc',
         'commands/prop_types.cc',
@@ -82,6 +85,9 @@
         'any_internal_impl_unittest.cc',
         'async_event_sequencer_unittest.cc',
         'buffet_testrunner.cc',
+        'commands/command_definition_unittest.cc',
+        'commands/command_dictionary_unittest.cc',
+        'commands/command_manager_unittest.cc',
         'commands/object_schema_unittest.cc',
         'commands/schema_utils_unittest.cc',
         'commands/unittest_utils.cc',
diff --git a/buffet/commands/command_definition.cc b/buffet/commands/command_definition.cc
new file mode 100644
index 0000000..f24b22d
--- /dev/null
+++ b/buffet/commands/command_definition.cc
@@ -0,0 +1,15 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/commands/command_definition.h"
+
+namespace buffet {
+
+CommandDefinition::CommandDefinition(
+    const std::string& category,
+    const std::shared_ptr<const ObjectSchema>& parameters)
+        : category_(category), parameters_(parameters) {
+}
+
+}  // namespace buffet
diff --git a/buffet/commands/command_definition.h b/buffet/commands/command_definition.h
new file mode 100644
index 0000000..36ba554
--- /dev/null
+++ b/buffet/commands/command_definition.h
@@ -0,0 +1,42 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_COMMANDS_COMMAND_DEFINITION_H_
+#define BUFFET_COMMANDS_COMMAND_DEFINITION_H_
+
+#include <memory>
+#include <string>
+
+#include <base/basictypes.h>
+
+#include "buffet/commands/object_schema.h"
+
+namespace buffet {
+
+// A simple GCD command definition. This class contains the command category
+// and a full object schema describing the command parameter types and
+// constraints. See comments for CommandDefinitions::LoadCommands for the
+// detailed description of what command categories are and what they are used
+// for.
+class CommandDefinition {
+ public:
+  CommandDefinition(const std::string& category,
+                    const std::shared_ptr<const ObjectSchema>& parameters);
+
+  // Gets the category this command belongs to.
+  const std::string& GetCategory() const { return category_; }
+  // Gets the object schema for command parameters.
+  const std::shared_ptr<const ObjectSchema>& GetParameters() const {
+    return parameters_;
+  }
+
+ private:
+  std::string category_;  // Cmd category. Could be "powerd" for "base.reboot".
+  std::shared_ptr<const ObjectSchema> parameters_;  // Command parameter def.
+  DISALLOW_COPY_AND_ASSIGN(CommandDefinition);
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_COMMANDS_COMMAND_DEFINITION_H_
diff --git a/buffet/commands/command_definition_unittest.cc b/buffet/commands/command_definition_unittest.cc
new file mode 100644
index 0000000..76212af
--- /dev/null
+++ b/buffet/commands/command_definition_unittest.cc
@@ -0,0 +1,14 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+
+#include "buffet/commands/command_definition.h"
+
+TEST(CommandDefinition, Test) {
+  auto params = std::make_shared<buffet::ObjectSchema>();
+  buffet::CommandDefinition def("powerd", params);
+  EXPECT_EQ("powerd", def.GetCategory());
+  EXPECT_EQ(params, def.GetParameters());
+}
diff --git a/buffet/commands/command_dictionary.cc b/buffet/commands/command_dictionary.cc
new file mode 100644
index 0000000..69f9e2b
--- /dev/null
+++ b/buffet/commands/command_dictionary.cc
@@ -0,0 +1,125 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/commands/command_dictionary.h"
+
+#include <base/values.h>
+
+#include "buffet/commands/command_definition.h"
+#include "buffet/commands/schema_constants.h"
+#include "buffet/string_utils.h"
+
+namespace buffet {
+
+std::vector<std::string> CommandDictionary::GetCommandNamesByCategory(
+    const std::string& category) const {
+  std::vector<std::string> names;
+  for (const auto& pair : definitions_) {
+    if (pair.second->GetCategory() == category)
+      names.push_back(pair.first);
+  }
+  return names;
+}
+
+bool CommandDictionary::LoadCommands(const base::DictionaryValue& json,
+                                     const std::string& category,
+                                     ErrorPtr* error) {
+  std::map<std::string, std::shared_ptr<const CommandDefinition>> new_defs;
+
+  // |json| contains a list of nested objects with the following structure:
+  // {"<pkg_name>": {"<cmd_name>": {"parameters": {object_schema}}, ...}, ...}
+  // Iterate over packages
+  base::DictionaryValue::Iterator package_iter(json);
+  while (!package_iter.IsAtEnd()) {
+    std::string package = package_iter.key();
+    const base::DictionaryValue* package_value = nullptr;
+    if (!package_iter.value().GetAsDictionary(&package_value)) {
+      Error::AddToPrintf(error, commands::errors::kDomain,
+                         commands::errors::kTypeMismatch,
+                         "Expecting an object for package '%s'",
+                         package.c_str());
+      return false;
+    }
+    // Iterate over command definitions within the current package.
+    base::DictionaryValue::Iterator command_iter(*package_value);
+    while (!command_iter.IsAtEnd()) {
+      std::string command = command_iter.key();
+      const base::DictionaryValue* command_value = nullptr;
+      if (!command_iter.value().GetAsDictionary(&command_value)) {
+        Error::AddToPrintf(error, commands::errors::kDomain,
+                           commands::errors::kTypeMismatch,
+                           "Expecting an object for command '%s'",
+                           command.c_str());
+        return false;
+      }
+      // Construct the compound command name as "pkg_name.cmd_name".
+      std::string command_name = string_utils::Join('.', package, command);
+      // Get the "parameters" definition of the command and read it into
+      // an object schema.
+      const base::DictionaryValue* command_schema_def = nullptr;
+      if (!command_value->GetDictionaryWithoutPathExpansion(
+          commands::attributes::kCommand_Parameters, &command_schema_def)) {
+        Error::AddToPrintf(error, commands::errors::kDomain,
+                           commands::errors::kPropertyMissing,
+                           "Command definition '%s' is missing property '%s'",
+                           command_name.c_str(),
+                           commands::attributes::kCommand_Parameters);
+        return false;
+      }
+      auto command_schema = std::make_shared<ObjectSchema>();
+      if (!command_schema->FromJson(command_schema_def, nullptr, error)) {
+        Error::AddToPrintf(error, commands::errors::kDomain,
+                           commands::errors::kInvalidObjectSchema,
+                           "Invalid definition for command '%s'",
+                           command_name.c_str());
+        return false;
+      }
+      auto command_def = std::make_shared<CommandDefinition>(category,
+                                                             command_schema);
+      new_defs.insert(std::make_pair(command_name, command_def));
+
+      command_iter.Advance();
+    }
+    package_iter.Advance();
+  }
+
+  // Verify that newly loaded command definitions do not override existing
+  // definitions in another category. This is unlikely, but we don't want to let
+  // one vendor daemon to define the same commands already handled by another
+  // daemon on the same device.
+  for (const auto& pair : new_defs) {
+    auto iter = definitions_.find(pair.first);
+    if (iter != definitions_.end()) {
+        Error::AddToPrintf(error, commands::errors::kDomain,
+                           commands::errors::kDuplicateCommandDef,
+                           "Definition for command '%s' overrides an earlier "
+                           "definition in category '%s'",
+                           pair.first.c_str(),
+                           iter->second->GetCategory().c_str());
+        return false;
+    }
+  }
+
+  // Now that we successfully loaded all the command definitions,
+  // remove previous definitions of commands from the same category.
+  std::vector<std::string> names = GetCommandNamesByCategory(category);
+  for (const std::string& name : names)
+    definitions_.erase(name);
+
+  // Insert new definitions into the global map.
+  definitions_.insert(new_defs.begin(), new_defs.end());
+  return true;
+}
+
+const CommandDefinition* CommandDictionary::FindCommand(
+    const std::string& command_name) const {
+  auto pair = definitions_.find(command_name);
+  return (pair != definitions_.end()) ? pair->second.get() : nullptr;
+}
+
+void CommandDictionary::Clear() {
+  definitions_.clear();
+}
+
+}  // namespace buffet
diff --git a/buffet/commands/command_dictionary.h b/buffet/commands/command_dictionary.h
new file mode 100644
index 0000000..11295c9
--- /dev/null
+++ b/buffet/commands/command_dictionary.h
@@ -0,0 +1,72 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_COMMANDS_COMMAND_DICTIONARY_H_
+#define BUFFET_COMMANDS_COMMAND_DICTIONARY_H_
+
+#include <map>
+#include <memory>
+#include <string>
+#include <vector>
+
+#include <base/basictypes.h>
+
+#include "buffet/error.h"
+
+namespace base {
+class Value;
+class DictionaryValue;
+}  // namespace base
+
+namespace buffet {
+
+class CommandDefinition;
+
+// CommandDictionary is a wrapper around a map of command name and the
+// corresponding command definition schema. The command name (the key in
+// the map) is a compound name in a form of "package_name.command_name",
+// where "package_name" is a name of command package such as "base", "printers",
+// and others. So the full command name could be "base.reboot", for example.
+class CommandDictionary {
+ public:
+  CommandDictionary() = default;
+
+  // Gets the list of names of commands that belong to the given category.
+  std::vector<std::string> GetCommandNamesByCategory(
+      const std::string& category) const;
+
+  // Loads command definitions from a JSON object. This is done at the daemon
+  // startup and whenever a device daemon decides to update its command list.
+  // |json| is a JSON dictionary that describes the complete commands contained
+  // in a particular |category|. Usually, |categories| are 1:1 with daemons on
+  // a device. For instance, the power manager daemon might provide a category
+  // "power_man" that provides the "base.reboot" and "base.shutdown" commands.
+  // However, nothing prohibits a daemon providing commands in two categories.
+  // When LoadCommands is called, all previous definitions of commands from the
+  // same category are removed, effectively replacing all the commands in the
+  // given category.
+  // Returns false on failure and |error| provides additional error information
+  // when provided.
+  bool LoadCommands(const base::DictionaryValue& json,
+                    const std::string& category, ErrorPtr* error);
+  // Returns the number of command definitions in the dictionary.
+  size_t GetSize() const { return definitions_.size(); }
+  // Checks if the dictionary has no command definitions.
+  bool IsEmpty() const { return definitions_.empty(); }
+  // Remove all the command definitions from the dictionary.
+  void Clear();
+  // Finds a definition for the given command.
+  const CommandDefinition* FindCommand(const std::string& command_name) const;
+
+ private:
+  using CommandMap = std::map<std::string,
+                              std::shared_ptr<const CommandDefinition>>;
+
+  CommandMap definitions_;  // List of all available command definitions.
+  DISALLOW_COPY_AND_ASSIGN(CommandDictionary);
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_COMMANDS_COMMAND_DICTIONARY_H_
diff --git a/buffet/commands/command_dictionary_unittest.cc b/buffet/commands/command_dictionary_unittest.cc
new file mode 100644
index 0000000..f345019
--- /dev/null
+++ b/buffet/commands/command_dictionary_unittest.cc
@@ -0,0 +1,97 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+
+#include "buffet/commands/command_dictionary.h"
+#include "buffet/commands/unittest_utils.h"
+
+using buffet::unittests::CreateDictionaryValue;
+
+TEST(CommandDictionary, Empty) {
+  buffet::CommandDictionary dict;
+  EXPECT_TRUE(dict.IsEmpty());
+  EXPECT_EQ(nullptr, dict.FindCommand("robot.jump"));
+  EXPECT_TRUE(dict.GetCommandNamesByCategory("robotd").empty());
+}
+
+TEST(CommandDictionary, LoadCommands) {
+  auto json = CreateDictionaryValue(R"({
+    'robot': {
+      'jump': {
+        'parameters': {
+          'height': 'integer',
+          '_jumpType': ['_withAirFlip', '_withSpin', '_withKick']
+        }
+      }
+    }
+  })");
+  buffet::CommandDictionary dict;
+  EXPECT_TRUE(dict.LoadCommands(*json, "robotd", nullptr));
+  EXPECT_EQ(1, dict.GetSize());
+  EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
+  json = CreateDictionaryValue(R"({
+    'base': {
+      'reboot': {
+        'parameters': {'delay': 'integer'}
+      },
+      'shutdown': {
+        'parameters': {}
+      }
+    }
+  })");
+  EXPECT_TRUE(dict.LoadCommands(*json, "powerd", nullptr));
+  EXPECT_EQ(3, dict.GetSize());
+  EXPECT_NE(nullptr, dict.FindCommand("robot.jump"));
+  EXPECT_NE(nullptr, dict.FindCommand("base.reboot"));
+  EXPECT_NE(nullptr, dict.FindCommand("base.shutdown"));
+  EXPECT_EQ(nullptr, dict.FindCommand("foo.bar"));
+  std::vector<std::string> expected_commands{"base.reboot", "base.shutdown"};
+  EXPECT_EQ(expected_commands, dict.GetCommandNamesByCategory("powerd"));
+}
+
+TEST(CommandDictionary, LoadCommands_Failures) {
+  buffet::CommandDictionary dict;
+  buffet::ErrorPtr error;
+
+  // Command definition missing 'parameters' property.
+  auto json = CreateDictionaryValue("{'robot':{'jump':{}}}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("parameter_missing", error->GetCode());
+  EXPECT_EQ("Command definition 'robot.jump' is missing property 'parameters'",
+            error->GetMessage());
+  error.reset();
+
+  // Command definition is not an object.
+  json = CreateDictionaryValue("{'robot':{'jump':0}}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("type_mismatch", error->GetCode());
+  EXPECT_EQ("Expecting an object for command 'jump'", error->GetMessage());
+  error.reset();
+
+  // Package definition is not an object.
+  json = CreateDictionaryValue("{'robot':'blah'}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("type_mismatch", error->GetCode());
+  EXPECT_EQ("Expecting an object for package 'robot'", error->GetMessage());
+  error.reset();
+
+  // Invalid command definition is not an object.
+  json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{'flip':0}}}}");
+  EXPECT_FALSE(dict.LoadCommands(*json, "robotd", &error));
+  EXPECT_EQ("invalid_object_schema", error->GetCode());
+  EXPECT_EQ("Invalid definition for command 'robot.jump'", error->GetMessage());
+  EXPECT_NE(nullptr, error->GetInnerError());  // Must have additional info.
+  error.reset();
+
+  // Redefine commands in different category.
+  json = CreateDictionaryValue("{'robot':{'jump':{'parameters':{}}}}");
+  dict.Clear();
+  dict.LoadCommands(*json, "category1", &error);
+  EXPECT_FALSE(dict.LoadCommands(*json, "category2", &error));
+  EXPECT_EQ("duplicate_command_definition", error->GetCode());
+  EXPECT_EQ("Definition for command 'robot.jump' overrides an earlier "
+            "definition in category 'category1'", error->GetMessage());
+  error.reset();
+}
diff --git a/buffet/commands/command_manager.cc b/buffet/commands/command_manager.cc
new file mode 100644
index 0000000..fc04b2d
--- /dev/null
+++ b/buffet/commands/command_manager.cc
@@ -0,0 +1,13 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include "buffet/commands/command_manager.h"
+
+namespace buffet {
+
+const CommandDictionary& CommandManager::GetCommandDictionary() const {
+  return dictionary_;
+}
+
+}  // namespace buffet
diff --git a/buffet/commands/command_manager.h b/buffet/commands/command_manager.h
new file mode 100644
index 0000000..1700712
--- /dev/null
+++ b/buffet/commands/command_manager.h
@@ -0,0 +1,31 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef BUFFET_COMMANDS_COMMAND_MANAGER_H_
+#define BUFFET_COMMANDS_COMMAND_MANAGER_H_
+
+#include <base/basictypes.h>
+
+#include "buffet/commands/command_dictionary.h"
+
+namespace buffet {
+
+// CommandManager class that will have a list of all the device command
+// schemas as well as the live command queue of pending command instances
+// dispatched to the device.
+class CommandManager {
+ public:
+  CommandManager() = default;
+
+  // Get the command definitions for the device.
+  const CommandDictionary& GetCommandDictionary() const;
+
+ private:
+  CommandDictionary dictionary_;  // Command definitions/schemas.
+  DISALLOW_COPY_AND_ASSIGN(CommandManager);
+};
+
+}  // namespace buffet
+
+#endif  // BUFFET_COMMANDS_COMMAND_MANAGER_H_
diff --git a/buffet/commands/command_manager_unittest.cc b/buffet/commands/command_manager_unittest.cc
new file mode 100644
index 0000000..1a0bfec
--- /dev/null
+++ b/buffet/commands/command_manager_unittest.cc
@@ -0,0 +1,12 @@
+// Copyright 2014 The Chromium OS Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#include <gtest/gtest.h>
+
+#include "buffet/commands/command_manager.h"
+
+TEST(CommandManager, Empty) {
+  buffet::CommandManager manager;
+  EXPECT_TRUE(manager.GetCommandDictionary().IsEmpty());
+}
diff --git a/buffet/commands/schema_constants.cc b/buffet/commands/schema_constants.cc
index d8ec689..6504c5d 100644
--- a/buffet/commands/schema_constants.cc
+++ b/buffet/commands/schema_constants.cc
@@ -20,6 +20,7 @@
 const char kPropertyMissing[] = "parameter_missing";
 const char kUnknownProperty[] = "unexpected_parameter";
 const char kInvalidObjectSchema[] = "invalid_object_schema";
+const char kDuplicateCommandDef[] = "duplicate_command_definition";
 }  // namespace errors
 
 namespace attributes {
@@ -37,6 +38,8 @@
 const char kOneOf_MetaSchema[] = "schema";
 
 const char kObject_Properties[] = "properties";
+
+const char kCommand_Parameters[] = "parameters";
 }  // namespace attributes
 
 }  // namespace commands
diff --git a/buffet/commands/schema_constants.h b/buffet/commands/schema_constants.h
index 087f53b..d709e59 100644
--- a/buffet/commands/schema_constants.h
+++ b/buffet/commands/schema_constants.h
@@ -23,6 +23,7 @@
 extern const char kPropertyMissing[];
 extern const char kUnknownProperty[];
 extern const char kInvalidObjectSchema[];
+extern const char kDuplicateCommandDef[];
 }  // namespace errors
 
 namespace attributes {
@@ -41,6 +42,8 @@
 extern const char kOneOf_MetaSchema[];
 
 extern const char kObject_Properties[];
+
+extern const char kCommand_Parameters[];
 }  // namespace attributes
 
 }  // namespace commands
diff --git a/buffet/commands/unittest_utils.h b/buffet/commands/unittest_utils.h
index e14e4d1..665a2d1 100644
--- a/buffet/commands/unittest_utils.h
+++ b/buffet/commands/unittest_utils.h
@@ -16,7 +16,7 @@
 // Helper method to create base::Value from a string as a smart pointer.
 // For ease of definition in C++ code, double-quotes in the source definition
 // are replaced with apostrophes.
-  std::unique_ptr<base::Value> CreateValue(const char* json);
+std::unique_ptr<base::Value> CreateValue(const char* json);
 
 // Helper method to create a JSON dictionary object from a string.
 std::unique_ptr<base::DictionaryValue> CreateDictionaryValue(const char* json);