Add "component" property to command instance

When sending commands, we'll use "component" to route the command to
the target component it was designated for.

As a temporary stop-gap, use "device" as the component name before
we have full implementation of component/trait schema model.

Also removed CommandDictionary from CommandInstance::FromJson since
the validation will be done outside of JSON parsing code in the future
Component Manager class.

BUG: 25841719
Change-Id: I5c649c257fb48ecaaedc1ced84931009f94c2bb3
Reviewed-on: https://weave-review.googlesource.com/1764
Reviewed-by: Vitaly Buka <vitalybuka@google.com>
diff --git a/src/base_api_handler_unittest.cc b/src/base_api_handler_unittest.cc
index 92b83a7..ded6315 100644
--- a/src/base_api_handler_unittest.cc
+++ b/src/base_api_handler_unittest.cc
@@ -72,12 +72,12 @@
   void AddCommand(const std::string& command) {
     auto command_instance = CommandInstance::FromJson(
         test::CreateDictionaryValue(command.c_str()).get(),
-        Command::Origin::kLocal, command_manager_->GetCommandDictionary(),
-        nullptr, nullptr);
+        Command::Origin::kLocal, nullptr, nullptr);
     EXPECT_TRUE(!!command_instance);
 
     std::string id{base::IntToString(++command_id_)};
     command_instance->SetID(id);
+    command_instance->SetComponent("device");
     command_manager_->AddCommand(std::move(command_instance));
     EXPECT_EQ(Command::State::kDone,
               command_manager_->FindCommand(id)->GetState());
diff --git a/src/commands/cloud_command_proxy_unittest.cc b/src/commands/cloud_command_proxy_unittest.cc
index 99ddffa..c022b79 100644
--- a/src/commands/cloud_command_proxy_unittest.cc
+++ b/src/commands/cloud_command_proxy_unittest.cc
@@ -76,28 +76,6 @@
     EXPECT_CALL(state_change_queue_, GetLastStateChangeId())
         .WillRepeatedly(testing::ReturnPointee(&current_state_update_id_));
 
-    // Set up the command schema.
-    auto json = CreateDictionaryValue(R"({
-      'calc': {
-        'add': {
-          'minimalRole': 'user',
-          'parameters': {
-            'value1': 'integer',
-            'value2': 'integer'
-          },
-          'progress': {
-            'status' : 'string'
-          },
-          'results': {
-            'sum' : 'integer'
-          }
-        }
-      }
-    })");
-    CHECK(json.get());
-    CHECK(command_dictionary_.LoadCommands(*json, nullptr))
-        << "Failed to parse test command dictionary";
-
     CreateCommandInstance();
   }
 
@@ -112,9 +90,8 @@
     })");
     CHECK(command_json.get());
 
-    command_instance_ =
-        CommandInstance::FromJson(command_json.get(), Command::Origin::kCloud,
-                                  command_dictionary_, nullptr, nullptr);
+    command_instance_ = CommandInstance::FromJson(
+        command_json.get(), Command::Origin::kCloud, nullptr, nullptr);
     CHECK(command_instance_.get());
 
     // Backoff - start at 1s and double with each backoff attempt and no jitter.
@@ -139,7 +116,6 @@
   testing::StrictMock<MockStateChangeQueueInterface> state_change_queue_;
   testing::StrictMock<provider::test::FakeTaskRunner> task_runner_;
   std::queue<base::Closure> task_queue_;
-  CommandDictionary command_dictionary_;
   std::unique_ptr<CommandInstance> command_instance_;
 };
 
diff --git a/src/commands/command_instance.cc b/src/commands/command_instance.cc
index 1f2e4a2..702a819 100644
--- a/src/commands/command_instance.cc
+++ b/src/commands/command_instance.cc
@@ -74,6 +74,10 @@
   return name_;
 }
 
+const std::string& CommandInstance::GetComponent() const {
+  return component_;
+}
+
 Command::State CommandInstance::GetState() const {
   return state_;
 }
@@ -169,7 +173,6 @@
 std::unique_ptr<CommandInstance> CommandInstance::FromJson(
     const base::Value* value,
     Command::Origin origin,
-    const CommandDictionary& dictionary,
     std::string* command_id,
     ErrorPtr* error) {
   std::unique_ptr<CommandInstance> instance;
@@ -198,14 +201,6 @@
                  errors::commands::kPropertyMissing, "Command name is missing");
     return instance;
   }
-  // Make sure we know how to handle the command with this name.
-  auto command_def = dictionary.FindCommand(command_name);
-  if (!command_def) {
-    Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
-                       errors::commands::kInvalidCommandName,
-                       "Unknown command received: %s", command_name.c_str());
-    return instance;
-  }
 
   auto parameters = GetCommandParameters(json, error);
   if (!parameters) {
@@ -220,6 +215,11 @@
   if (!command_id->empty())
     instance->SetID(*command_id);
 
+  // Get the component name this command is for.
+  std::string component;
+  if (json->GetString(commands::attributes::kCommand_Component, &component))
+    instance->SetComponent(component);
+
   return instance;
 }
 
diff --git a/src/commands/command_instance.h b/src/commands/command_instance.h
index 82df190..15f3ac2 100644
--- a/src/commands/command_instance.h
+++ b/src/commands/command_instance.h
@@ -50,6 +50,7 @@
   // Command overrides.
   const std::string& GetID() const override;
   const std::string& GetName() const override;
+  const std::string& GetComponent() const override;
   Command::State GetState() const override;
   Command::Origin GetOrigin() const override;
   const base::DictionaryValue& GetParameters() const override;
@@ -65,9 +66,8 @@
   bool Cancel(ErrorPtr* error) override;
 
   // Parses a command instance JSON definition and constructs a CommandInstance
-  // object, checking the JSON |value| against the command definition schema
-  // found in command |dictionary|. On error, returns null unique_ptr and
-  // fills in error details in |error|.
+  // object.
+  // On error, returns null unique_ptr and fills in error details in |error|.
   // |command_id| is the ID of the command returned, as parsed from the |value|.
   // The command ID extracted (if present in the JSON object) even if other
   // parsing/validation error occurs and command instance is not constructed.
@@ -75,7 +75,6 @@
   static std::unique_ptr<CommandInstance> FromJson(
       const base::Value* value,
       Command::Origin origin,
-      const CommandDictionary& dictionary,
       std::string* command_id,
       ErrorPtr* error);
 
@@ -84,6 +83,7 @@
   // Sets the command ID (normally done by CommandQueue when the command
   // instance is added to it).
   void SetID(const std::string& id) { id_ = id; }
+  void SetComponent(const std::string& component) { component_ = component; }
 
   void AddObserver(Observer* observer);
   void RemoveObserver(Observer* observer);
@@ -106,8 +106,10 @@
 
   // Unique command ID within a command queue.
   std::string id_;
-  // Full command name as "<package_name>.<command_name>".
+  // Full command name as "<trait_name>.<command_name>".
   std::string name_;
+  // Full path to the component this command is intended for.
+  std::string component_;
   // The origin of the command, either "local" or "cloud".
   Command::Origin origin_ = Command::Origin::kLocal;
   // Command parameters and their values.
diff --git a/src/commands/command_instance_unittest.cc b/src/commands/command_instance_unittest.cc
index 7c8aa2d..6f55ad2 100644
--- a/src/commands/command_instance_unittest.cc
+++ b/src/commands/command_instance_unittest.cc
@@ -14,63 +14,7 @@
 using test::CreateDictionaryValue;
 using test::CreateValue;
 
-namespace {
-
-class CommandInstanceTest : public ::testing::Test {
- protected:
-  void SetUp() override {
-    auto json = CreateDictionaryValue(R"({
-      'base': {
-        'reboot': {
-          'minimalRole': 'user',
-          'parameters': {},
-          'results': {}
-        }
-      },
-      'robot': {
-        'jump': {
-          'minimalRole': 'user',
-          'parameters': {
-            'height': {
-              'type': 'integer',
-              'minimum': 0,
-              'maximum': 100
-            },
-            '_jumpType': {
-              'type': 'string',
-              'enum': ['_withAirFlip', '_withSpin', '_withKick']
-            }
-          },
-          'progress': {'progress': 'integer'},
-          'results': {'testResult': 'integer'}
-        },
-        'speak': {
-          'minimalRole': 'user',
-          'parameters': {
-            'phrase': {
-              'type': 'string',
-              'enum': ['beamMeUpScotty', 'iDontDigOnSwine',
-                       'iPityDaFool', 'dangerWillRobinson']
-            },
-            'volume': {
-              'type': 'integer',
-              'minimum': 0,
-              'maximum': 10
-            }
-          },
-          'results': {'foo': 'integer'}
-        }
-      }
-    })");
-    CHECK(dict_.LoadCommands(*json, nullptr))
-        << "Failed to parse test command dictionary";
-  }
-  CommandDictionary dict_;
-};
-
-}  // anonymous namespace
-
-TEST_F(CommandInstanceTest, Test) {
+TEST(CommandInstanceTest, Test) {
   auto params = CreateDictionaryValue(R"({
     'phrase': 'iPityDaFool',
     'volume': 5
@@ -91,15 +35,16 @@
   EXPECT_EQ(Command::Origin::kLocal, instance2.GetOrigin());
 }
 
-TEST_F(CommandInstanceTest, SetID) {
+TEST(CommandInstanceTest, SetID) {
   CommandInstance instance{"base.reboot", Command::Origin::kLocal, {}};
   instance.SetID("command_id");
   EXPECT_EQ("command_id", instance.GetID());
 }
 
-TEST_F(CommandInstanceTest, FromJson) {
+TEST(CommandInstanceTest, FromJson) {
   auto json = CreateDictionaryValue(R"({
     'name': 'robot.jump',
+    'component': 'comp1.comp2',
     'id': 'abcd',
     'parameters': {
       'height': 53,
@@ -109,64 +54,56 @@
   })");
   std::string id;
   auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, &id, nullptr);
+                                            &id, nullptr);
   EXPECT_EQ("abcd", id);
   EXPECT_EQ("abcd", instance->GetID());
   EXPECT_EQ("robot.jump", instance->GetName());
+  EXPECT_EQ("comp1.comp2", instance->GetComponent());
   EXPECT_JSON_EQ("{'height': 53, '_jumpType': '_withKick'}",
                  instance->GetParameters());
 }
 
-TEST_F(CommandInstanceTest, FromJson_ParamsOmitted) {
+TEST(CommandInstanceTest, FromJson_ParamsOmitted) {
   auto json = CreateDictionaryValue("{'name': 'base.reboot'}");
   auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, nullptr, nullptr);
+                                            nullptr, nullptr);
   EXPECT_EQ("base.reboot", instance->GetName());
   EXPECT_JSON_EQ("{}", instance->GetParameters());
 }
 
-TEST_F(CommandInstanceTest, FromJson_NotObject) {
+TEST(CommandInstanceTest, FromJson_NotObject) {
   auto json = CreateValue("'string'");
   ErrorPtr error;
   auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, nullptr, &error);
+                                            nullptr, &error);
   EXPECT_EQ(nullptr, instance.get());
   EXPECT_EQ("json_object_expected", error->GetCode());
 }
 
-TEST_F(CommandInstanceTest, FromJson_NameMissing) {
+TEST(CommandInstanceTest, FromJson_NameMissing) {
   auto json = CreateDictionaryValue("{'param': 'value'}");
   ErrorPtr error;
   auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, nullptr, &error);
+                                            nullptr, &error);
   EXPECT_EQ(nullptr, instance.get());
   EXPECT_EQ("parameter_missing", error->GetCode());
 }
 
-TEST_F(CommandInstanceTest, FromJson_UnknownCommand) {
-  auto json = CreateDictionaryValue("{'name': 'robot.scream'}");
-  ErrorPtr error;
-  auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, nullptr, &error);
-  EXPECT_EQ(nullptr, instance.get());
-  EXPECT_EQ("invalid_command_name", error->GetCode());
-}
-
-TEST_F(CommandInstanceTest, FromJson_ParamsNotObject) {
+TEST(CommandInstanceTest, FromJson_ParamsNotObject) {
   auto json = CreateDictionaryValue(R"({
     'name': 'robot.speak',
     'parameters': 'hello'
   })");
   ErrorPtr error;
   auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, nullptr, &error);
+                                            nullptr, &error);
   EXPECT_EQ(nullptr, instance.get());
   auto inner = error->GetInnerError();
   EXPECT_EQ("json_object_expected", inner->GetCode());
   EXPECT_EQ("command_failed", error->GetCode());
 }
 
-TEST_F(CommandInstanceTest, ToJson) {
+TEST(CommandInstanceTest, ToJson) {
   auto json = CreateDictionaryValue(R"({
     'name': 'robot.jump',
     'parameters': {
@@ -176,7 +113,7 @@
     'results': {}
   })");
   auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, nullptr, nullptr);
+                                            nullptr, nullptr);
   EXPECT_TRUE(instance->SetProgress(*CreateDictionaryValue("{'progress': 15}"),
                                     nullptr));
   EXPECT_TRUE(instance->SetProgress(*CreateDictionaryValue("{'progress': 15}"),
@@ -198,13 +135,13 @@
                *json, *converted);
 }
 
-TEST_F(CommandInstanceTest, ToJsonError) {
+TEST(CommandInstanceTest, ToJsonError) {
   auto json = CreateDictionaryValue(R"({
     'name': 'base.reboot',
     'parameters': {}
   })");
   auto instance = CommandInstance::FromJson(json.get(), Command::Origin::kCloud,
-                                            dict_, nullptr, nullptr);
+                                            nullptr, nullptr);
   instance->SetID("testId");
 
   ErrorPtr error;
diff --git a/src/commands/command_manager.cc b/src/commands/command_manager.cc
index 8d4602b..9e9852b 100644
--- a/src/commands/command_manager.cc
+++ b/src/commands/command_manager.cc
@@ -57,9 +57,8 @@
                                 UserRole role,
                                 std::string* id,
                                 ErrorPtr* error) {
-  auto command_instance =
-      CommandInstance::FromJson(&command, Command::Origin::kLocal,
-                                GetCommandDictionary(), nullptr, error);
+  auto command_instance = CommandInstance::FromJson(
+      &command, Command::Origin::kLocal, nullptr, error);
   if (!command_instance)
     return false;
 
@@ -76,6 +75,7 @@
     return false;
   }
 
+  command_instance->SetComponent("device");
   *id = std::to_string(++next_command_id_);
   command_instance->SetID(*id);
   AddCommand(std::move(command_instance));
@@ -101,7 +101,7 @@
     const Device::CommandHandlerCallback& callback) {
   CHECK(command_name.empty() || dictionary_.FindCommand(command_name))
       << "Command undefined: " << command_name;
-  command_queue_.AddCommandHandler(command_name, callback);
+  command_queue_.AddCommandHandler("device", command_name, callback);
 }
 
 }  // namespace weave
diff --git a/src/commands/command_manager.h b/src/commands/command_manager.h
index 04f49ee..644c165 100644
--- a/src/commands/command_manager.h
+++ b/src/commands/command_manager.h
@@ -44,10 +44,6 @@
   // Returns the command definitions for the device.
   const CommandDictionary& GetCommandDictionary() const;
 
-  // Same as the overload above, but takes a path to a json file to read
-  // the base command definitions from.
-  bool LoadStandardCommands(const std::string& json, ErrorPtr* error);
-
   // Loads device command schema.
   bool LoadCommands(const base::DictionaryValue& dict,
                     ErrorPtr* error);
diff --git a/src/commands/command_queue.cc b/src/commands/command_queue.cc
index 3d2167f..134dc1c 100644
--- a/src/commands/command_queue.cc
+++ b/src/commands/command_queue.cc
@@ -11,6 +11,11 @@
 
 namespace {
 const int kRemoveCommandDelayMin = 5;
+
+std::string GetCommandHandlerKey(const std::string& component_path,
+                                 const std::string& command_name) {
+  return component_path + ":" + command_name;
+}
 }
 
 void CommandQueue::AddCommandAddedCallback(const CommandCallback& callback) {
@@ -25,6 +30,7 @@
 }
 
 void CommandQueue::AddCommandHandler(
+    const std::string& component_path,
     const std::string& command_name,
     const Device::CommandHandlerCallback& callback) {
   if (!command_name.empty()) {
@@ -33,20 +39,24 @@
 
     for (const auto& command : map_) {
       if (command.second->GetState() == Command::State::kQueued &&
-          command.second->GetName() == command_name) {
+          command.second->GetName() == command_name &&
+          command.second->GetComponent() == component_path) {
         callback.Run(command.second);
       }
     }
 
-    CHECK(command_callbacks_.insert(std::make_pair(command_name, callback))
-              .second)
+    std::string key = GetCommandHandlerKey(component_path, command_name);
+    CHECK(command_callbacks_.insert(std::make_pair(key, callback)).second)
         << command_name << " already has handler";
 
   } else {
+    CHECK(component_path.empty())
+        << "Default handler must not be component-specific";
     for (const auto& command : map_) {
+      std::string key = GetCommandHandlerKey(command.second->GetComponent(),
+                                             command.second->GetName());
       if (command.second->GetState() == Command::State::kQueued &&
-          command_callbacks_.find(command.second->GetName()) ==
-              command_callbacks_.end()) {
+          command_callbacks_.find(key) == command_callbacks_.end()) {
         callback.Run(command.second);
       }
     }
@@ -66,7 +76,9 @@
   for (const auto& cb : on_command_added_)
     cb.Run(pair.first->second.get());
 
-  auto it_handler = command_callbacks_.find(pair.first->second->GetName());
+  std::string key = GetCommandHandlerKey(pair.first->second->GetComponent(),
+                                         pair.first->second->GetName());
+  auto it_handler = command_callbacks_.find(key);
 
   if (it_handler != command_callbacks_.end())
     it_handler->second.Run(pair.first->second);
diff --git a/src/commands/command_queue.h b/src/commands/command_queue.h
index 74ed86f..0f0a18b 100644
--- a/src/commands/command_queue.h
+++ b/src/commands/command_queue.h
@@ -34,7 +34,8 @@
   // Adds notifications callback for a command is removed from the queue.
   void AddCommandRemovedCallback(const CommandCallback& callback);
 
-  void AddCommandHandler(const std::string& command_name,
+  void AddCommandHandler(const std::string& component_path,
+                         const std::string& command_name,
                          const Device::CommandHandlerCallback& callback);
 
   // Checks if the command queue is empty.
diff --git a/src/commands/schema_constants.cc b/src/commands/schema_constants.cc
index cd8bf84..34d6db8 100644
--- a/src/commands/schema_constants.cc
+++ b/src/commands/schema_constants.cc
@@ -26,6 +26,7 @@
 
 const char kCommand_Id[] = "id";
 const char kCommand_Name[] = "name";
+const char kCommand_Component[] = "component";
 const char kCommand_Parameters[] = "parameters";
 const char kCommand_Progress[] = "progress";
 const char kCommand_Results[] = "results";
diff --git a/src/commands/schema_constants.h b/src/commands/schema_constants.h
index 57766e6..9199480 100644
--- a/src/commands/schema_constants.h
+++ b/src/commands/schema_constants.h
@@ -29,6 +29,7 @@
 // Command description JSON schema attributes.
 extern const char kCommand_Id[];
 extern const char kCommand_Name[];
+extern const char kCommand_Component[];
 extern const char kCommand_Parameters[];
 extern const char kCommand_Progress[];
 extern const char kCommand_Results[];
diff --git a/src/device_registration_info.cc b/src/device_registration_info.cc
index 33b1643..9469d09 100644
--- a/src/device_registration_info.cc
+++ b/src/device_registration_info.cc
@@ -1103,8 +1103,7 @@
   std::string command_id;
   ErrorPtr error;
   auto command_instance = CommandInstance::FromJson(
-      &command, Command::Origin::kCloud,
-      command_manager_->GetCommandDictionary(), &command_id, &error);
+      &command, Command::Origin::kCloud, &command_id, &error);
   if (!command_instance) {
     LOG(WARNING) << "Failed to parse a command instance: " << command;
     if (!command_id.empty())