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);