Add ComponentManager class to handle device traits/components

This is an initial implementation of component manager which will
handle device trait and component definitions, command queue and
state.

This implementation has no handling for state or state update
notifications yet.

BUG: 25841719
Change-Id: I29c5542851798dfa4e5ea1e77b6471b8dd276fb0
Reviewed-on: https://weave-review.googlesource.com/1739
Reviewed-by: Vitaly Buka <vitalybuka@google.com>
diff --git a/libweave.gypi b/libweave.gypi
index 5930e8f..f74a9bf 100644
--- a/libweave.gypi
+++ b/libweave.gypi
@@ -12,6 +12,7 @@
       'src/commands/command_manager.cc',
       'src/commands/command_queue.cc',
       'src/commands/schema_constants.cc',
+      'src/component_manager.cc',
       'src/config.cc',
       'src/data_encoding.cc',
       'src/device_manager.cc',
@@ -64,6 +65,7 @@
       'src/commands/command_instance_unittest.cc',
       'src/commands/command_manager_unittest.cc',
       'src/commands/command_queue_unittest.cc',
+      'src/component_manager_unittest.cc',
       'src/config_unittest.cc',
       'src/data_encoding_unittest.cc',
       'src/device_registration_info_unittest.cc',
diff --git a/src/component_manager.cc b/src/component_manager.cc
new file mode 100644
index 0000000..67a0524
--- /dev/null
+++ b/src/component_manager.cc
@@ -0,0 +1,376 @@
+// Copyright 2015 The Weave 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 "src/component_manager.h"
+
+#include <base/strings/stringprintf.h>
+#include <base/strings/string_number_conversions.h>
+#include <base/strings/string_util.h>
+
+#include "src/commands/schema_constants.h"
+#include "src/string_utils.h"
+#include "src/utils.h"
+
+namespace weave {
+
+ComponentManager::ComponentManager() {}
+ComponentManager::~ComponentManager() {}
+
+bool ComponentManager::AddComponent(const std::string& path,
+                                    const std::string& name,
+                                    const std::vector<std::string>& traits,
+                                    ErrorPtr* error) {
+  base::DictionaryValue* root = &components_;
+  if (!path.empty()) {
+    root = FindComponentGraftNode(path, error);
+    if (!root)
+      return false;
+  }
+  if (root->GetWithoutPathExpansion(name, nullptr)) {
+    Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                       errors::commands::kInvalidState,
+                       "Component '%s' already exists at path '%s'",
+                       name.c_str(), path.c_str());
+    return false;
+  }
+
+  // Check to make sure the declared traits are already defined.
+  for (const std::string& trait : traits) {
+    if (!FindTraitDefinition(trait)) {
+      Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                         errors::commands::kInvalidPropValue,
+                         "Trait '%s' is undefined", trait.c_str());
+      return false;
+    }
+  }
+  std::unique_ptr<base::DictionaryValue> dict{new base::DictionaryValue};
+  std::unique_ptr<base::ListValue> traits_list{new base::ListValue};
+  traits_list->AppendStrings(traits);
+  dict->Set("traits", traits_list.release());
+  root->SetWithoutPathExpansion(name, dict.release());
+  return true;
+}
+
+bool ComponentManager::AddComponentArrayItem(
+    const std::string& path,
+    const std::string& name,
+    const std::vector<std::string>& traits,
+    ErrorPtr* error) {
+  base::DictionaryValue* root = &components_;
+  if (!path.empty()) {
+    root = FindComponentGraftNode(path, error);
+    if (!root)
+      return false;
+  }
+  base::ListValue* array_value = nullptr;
+  if (!root->GetListWithoutPathExpansion(name, &array_value)) {
+    array_value = new base::ListValue;
+    root->SetWithoutPathExpansion(name, array_value);
+  }
+  std::unique_ptr<base::DictionaryValue> dict{new base::DictionaryValue};
+  std::unique_ptr<base::ListValue> traits_list{new base::ListValue};
+  traits_list->AppendStrings(traits);
+  dict->Set("traits", traits_list.release());
+  array_value->Append(dict.release());
+  return true;
+}
+
+bool ComponentManager::LoadTraits(const base::DictionaryValue& dict,
+                                  ErrorPtr* error) {
+  bool modified = false;
+  bool result = true;
+  // Check if any of the new traits are already defined. If so, make sure the
+  // definition is exactly the same, or else this is an error.
+  for (base::DictionaryValue::Iterator it(dict); !it.IsAtEnd(); it.Advance()) {
+    if (it.value().GetType() != base::Value::TYPE_DICTIONARY) {
+      Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                         errors::commands::kTypeMismatch,
+                         "Trait '%s' must be an object", it.key().c_str());
+      result = false;
+      break;
+    }
+    const base::DictionaryValue* existing_def = nullptr;
+    if (traits_.GetDictionary(it.key(), &existing_def)) {
+      if (!existing_def->Equals(&it.value())) {
+        Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                           errors::commands::kTypeMismatch,
+                           "Trait '%s' cannot be redefined",
+                           it.key().c_str());
+        result = false;
+        break;
+      }
+    }
+    traits_.Set(it.key(), it.value().DeepCopy());
+    modified = true;
+  }
+
+  if (modified) {
+    for (const auto& cb : on_trait_changed_)
+      cb.Run();
+  }
+  return result;
+}
+
+bool ComponentManager::LoadTraits(const std::string& json, ErrorPtr* error) {
+  std::unique_ptr<const base::DictionaryValue> dict = LoadJsonDict(json, error);
+  if (!dict)
+    return false;
+  return LoadTraits(*dict, error);
+}
+
+void ComponentManager::AddTraitDefChanged(const base::Closure& callback) {
+  on_trait_changed_.push_back(callback);
+  callback.Run();
+}
+
+void ComponentManager::AddCommand(
+    std::unique_ptr<CommandInstance> command_instance) {
+  command_queue_.Add(std::move(command_instance));
+}
+
+bool ComponentManager::AddCommand(const base::DictionaryValue& command,
+                                  UserRole role,
+                                  std::string* id,
+                                  ErrorPtr* error) {
+  auto command_instance = CommandInstance::FromJson(
+      &command, Command::Origin::kLocal, nullptr, error);
+  if (!command_instance)
+    return false;
+
+  UserRole minimal_role;
+  if (!GetMinimalRole(command_instance->GetName(), &minimal_role, error))
+    return false;
+
+  if (role < minimal_role) {
+    Error::AddToPrintf(
+        error, FROM_HERE, errors::commands::kDomain, "access_denied",
+        "User role '%s' less than minimal: '%s'", EnumToString(role).c_str(),
+        EnumToString(minimal_role).c_str());
+    return false;
+  }
+
+  std::string component_path = command_instance->GetComponent();
+  if (component_path.empty()) {
+    // Get the name of the first top-level component.
+    base::DictionaryValue::Iterator it(components_);
+    if (it.IsAtEnd()) {
+      Error::AddTo(error, FROM_HERE, errors::commands::kDomain,
+                   "component_not_found", "There are no components defined");
+      return false;
+    }
+    component_path = it.key();
+    command_instance->SetComponent(component_path);
+  }
+
+  const auto* component = FindComponent(component_path, error);
+  if (!component)
+    return false;
+
+  // Check that the command's trait is supported by the given component.
+  auto pair = SplitAtFirst(command_instance->GetName(), ".", true);
+
+  bool trait_supported = false;
+  const base::ListValue* supported_traits = nullptr;
+  if (component->GetList("traits", &supported_traits)) {
+    for (const base::Value* value : *supported_traits) {
+      std::string trait;
+      CHECK(value->GetAsString(&trait));
+      if (trait == pair.first) {
+        trait_supported = true;
+        break;
+      }
+    }
+  }
+
+  if (!trait_supported) {
+    Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                       "trait_not_supported",
+                       "Component '%s' doesn't support trait '%s'",
+                       component_path.c_str(), pair.first.c_str());
+    return false;
+  }
+
+  std::string command_id = std::to_string(++next_command_id_);
+  command_instance->SetID(command_id);
+  if (id)
+    *id = command_id;
+  AddCommand(std::move(command_instance));
+  return true;
+}
+
+CommandInstance* ComponentManager::FindCommand(const std::string& id) {
+  return command_queue_.Find(id);
+}
+
+void ComponentManager::AddCommandAddedCallback(
+    const CommandQueue::CommandCallback& callback) {
+  command_queue_.AddCommandAddedCallback(callback);
+}
+
+void ComponentManager::AddCommandRemovedCallback(
+    const CommandQueue::CommandCallback& callback) {
+  command_queue_.AddCommandRemovedCallback(callback);
+}
+
+void ComponentManager::AddCommandHandler(
+    const std::string& component_path,
+    const std::string& command_name,
+    const Device::CommandHandlerCallback& callback) {
+  CHECK(FindCommandDefinition(command_name))
+      << "Command undefined: " << command_name;
+  command_queue_.AddCommandHandler(component_path, command_name, callback);
+}
+
+const base::DictionaryValue* ComponentManager::FindComponent(
+    const std::string& path, ErrorPtr* error) const {
+  return FindComponentAt(&components_, path, error);
+}
+
+const base::DictionaryValue* ComponentManager::FindTraitDefinition(
+    const std::string& name) const {
+  const base::DictionaryValue* trait = nullptr;
+  traits_.GetDictionaryWithoutPathExpansion(name, &trait);
+  return trait;
+}
+
+const base::DictionaryValue* ComponentManager::FindCommandDefinition(
+    const std::string& command_name) const {
+  const base::DictionaryValue* definition = nullptr;
+  std::vector<std::string> components = Split(command_name, ".", true, false);
+  // Make sure the |command_name| came in form of trait_name.command_name.
+  if (components.size() != 2)
+    return definition;
+  std::string key = base::StringPrintf("%s.commands.%s", components[0].c_str(),
+                                       components[1].c_str());
+  traits_.GetDictionary(key, &definition);
+  return definition;
+}
+
+bool ComponentManager::GetMinimalRole(const std::string& command_name,
+                                      UserRole* minimal_role,
+                                      ErrorPtr* error) const {
+  const base::DictionaryValue* command = FindCommandDefinition(command_name);
+  if (!command) {
+    Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                       errors::commands::kInvalidCommandName,
+                       "Command definition for '%s' not found",
+                       command_name.c_str());
+    return false;
+  }
+  std::string value;
+  // The JSON definition has been pre-validated already in LoadCommands, so
+  // just using CHECKs here.
+  CHECK(command->GetString(commands::attributes::kCommand_Role, &value));
+  CHECK(StringToEnum(value, minimal_role));
+  return true;
+}
+
+base::DictionaryValue* ComponentManager::FindComponentGraftNode(
+    const std::string& path, ErrorPtr* error) {
+  base::DictionaryValue* root = nullptr;
+  auto component = const_cast<base::DictionaryValue*>(FindComponentAt(
+      &components_, path, error));
+  if (component && !component->GetDictionary("components", &root)) {
+    root = new base::DictionaryValue;
+    component->Set("components", root);
+  }
+  return root;
+}
+
+const base::DictionaryValue* ComponentManager::FindComponentAt(
+    const base::DictionaryValue* root,
+    const std::string& path,
+    ErrorPtr* error) {
+  auto parts = Split(path, ".", true, false);
+  std::string root_path;
+  for (size_t i = 0; i < parts.size(); i++) {
+    auto element = SplitAtFirst(parts[i], "[", true);
+    int array_index = -1;
+    if (element.first.empty()) {
+      Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                         errors::commands::kPropertyMissing,
+                         "Empty path element at '%s'", root_path.c_str());
+      return nullptr;
+    }
+    if (!element.second.empty()) {
+      if (element.second.back() != ']') {
+        Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                           errors::commands::kPropertyMissing,
+                           "Invalid array element syntax '%s'",
+                           parts[i].c_str());
+        return nullptr;
+      }
+      element.second.pop_back();
+      std::string index_str;
+      base::TrimWhitespaceASCII(element.second, base::TrimPositions::TRIM_ALL,
+                                &index_str);
+      if (!base::StringToInt(index_str, &array_index) || array_index < 0) {
+        Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                           errors::commands::kInvalidPropValue,
+                           "Invalid array index '%s'", element.second.c_str());
+        return nullptr;
+      }
+    }
+
+    if (!root_path.empty()) {
+      // We have processed at least one item in the path before, so now |root|
+      // points to the actual parent component. We need the root to point to
+      // the 'components' element containing child sub-components instead.
+      if (!root->GetDictionary("components", &root)) {
+        Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                           errors::commands::kPropertyMissing,
+                           "Component '%s' does not exist at '%s'",
+                           element.first.c_str(), root_path.c_str());
+        return nullptr;
+      }
+    }
+
+    const base::Value* value = nullptr;
+    if (!root->GetWithoutPathExpansion(element.first, &value)) {
+      Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                         errors::commands::kPropertyMissing,
+                         "Component '%s' does not exist at '%s'",
+                         element.first.c_str(), root_path.c_str());
+      return nullptr;
+    }
+
+    if (value->GetType() == base::Value::TYPE_LIST && array_index < 0) {
+      Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                         errors::commands::kTypeMismatch,
+                         "Element '%s.%s' is an array",
+                         root_path.c_str(), element.first.c_str());
+      return nullptr;
+    }
+    if (value->GetType() == base::Value::TYPE_DICTIONARY && array_index >= 0) {
+      Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                         errors::commands::kTypeMismatch,
+                         "Element '%s.%s' is not an array",
+                         root_path.c_str(), element.first.c_str());
+      return nullptr;
+    }
+
+    if (value->GetType() == base::Value::TYPE_DICTIONARY) {
+      CHECK(value->GetAsDictionary(&root));
+    } else {
+      const base::ListValue* component_array = nullptr;
+      CHECK(value->GetAsList(&component_array));
+      const base::Value* component_value = nullptr;
+      if (!component_array->Get(array_index, &component_value) ||
+          !component_value->GetAsDictionary(&root)) {
+        Error::AddToPrintf(error, FROM_HERE, errors::commands::kDomain,
+                           errors::commands::kPropertyMissing,
+                           "Element '%s.%s' does not contain item #%d",
+                           root_path.c_str(), element.first.c_str(),
+                           array_index);
+        return nullptr;
+      }
+    }
+    if (!root_path.empty())
+      root_path += '.';
+    root_path += parts[i];
+  }
+  return root;
+}
+
+}  // namespace weave
diff --git a/src/component_manager.h b/src/component_manager.h
new file mode 100644
index 0000000..d9de8ea
--- /dev/null
+++ b/src/component_manager.h
@@ -0,0 +1,130 @@
+// Copyright 2015 The Weave 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 LIBWEAVE_SRC_COMPONENT_MANAGER_H_
+#define LIBWEAVE_SRC_COMPONENT_MANAGER_H_
+
+#include <memory>
+
+#include <base/values.h>
+#include <weave/error.h>
+
+#include "src/commands/command_dictionary.h"
+#include "src/commands/command_queue.h"
+
+namespace weave {
+
+class CommandInstance;
+
+class ComponentManager final {
+ public:
+  ComponentManager();
+  ~ComponentManager();
+
+  // Loads trait definition schema.
+  bool LoadTraits(const base::DictionaryValue& dict, ErrorPtr* error);
+
+  // Same as the overload above, but takes a json string to read the trait
+  // definitions from.
+  bool LoadTraits(const std::string& json, ErrorPtr* error);
+
+  // Sets callback which is called when new trait definitions are added.
+  void AddTraitDefChanged(const base::Closure& callback);
+
+  // Adds a new component instance to device.
+  // |path| is a path to the parent component (or empty string if a root-level
+  // component is being added).
+  // |name| is a component name being added.
+  // |traits| is a list of trait names this component supports.
+  bool AddComponent(const std::string& path,
+                    const std::string& name,
+                    const std::vector<std::string>& traits,
+                    ErrorPtr* error);
+
+  // Adds a new component instance to device, as a part of component array.
+  // |path| is a path to the parent component.
+  // |name| is an array root element inside the child components.
+  // |traits| is a list of trait names this component supports.
+  bool AddComponentArrayItem(const std::string& path,
+                             const std::string& name,
+                             const std::vector<std::string>& traits,
+                             ErrorPtr* error);
+
+  // Adds a new command instance to the command queue. The command specified in
+  // |command_instance| must be fully initialized and have its name, component,
+  // id populated.
+  void AddCommand(std::unique_ptr<CommandInstance> command_instance);
+
+  // Parses the command definition from a json dictionary and adds it to the
+  // command queue. The new command ID is returned through optional |id| param.
+  bool AddCommand(const base::DictionaryValue& command,
+                  UserRole role,
+                  std::string* id,
+                  ErrorPtr* error);
+
+  // Find a command instance with the given ID in the command queue.
+  CommandInstance* FindCommand(const std::string& id);
+
+  // Command queue monitoring callbacks (called when a new command is added to
+  // or removed from the queue).
+  void AddCommandAddedCallback(const CommandQueue::CommandCallback& callback);
+  void AddCommandRemovedCallback(const CommandQueue::CommandCallback& callback);
+
+  // Adds a command handler for specific component's command.
+  // |component_path| is a path to target component (e.g. "stove.burners[2]").
+  // |command_name| is a full path of the command, including trait name and
+  // command name (e.g. "burner.setPower").
+  void AddCommandHandler(const std::string& component_path,
+                         const std::string& command_name,
+                         const Device::CommandHandlerCallback& callback);
+
+  // Finds a component instance by its full path.
+  const base::DictionaryValue* FindComponent(const std::string& path,
+                                             ErrorPtr* error) const;
+  // Finds a definition of trait with the given |name|.
+  const base::DictionaryValue* FindTraitDefinition(
+      const std::string& name) const;
+
+  // Finds a command definition, where |command_name| is in the form of
+  // "trait.command".
+  const base::DictionaryValue* FindCommandDefinition(
+      const std::string& command_name) const;
+
+  // Checks the minimum required user role for a given command.
+  bool GetMinimalRole(const std::string& command_name,
+                      UserRole* minimal_role,
+                      ErrorPtr* error) const;
+
+  // Returns the full JSON dictionary containing trait definitions.
+  const base::DictionaryValue& GetTraits() const { return traits_; }
+
+  // Returns the full JSON dictionary containing component instances.
+  const base::DictionaryValue& GetComponents() const { return components_; }
+
+ private:
+  // A helper method to find a JSON element of component at |path| to add new
+  // sub-components to.
+  base::DictionaryValue* FindComponentGraftNode(const std::string& path,
+                                                ErrorPtr* error);
+
+  // Helper method to find a sub-component given a root node and a relative path
+  // from the root to the target component.
+  static const base::DictionaryValue* FindComponentAt(
+      const base::DictionaryValue* root,
+      const std::string& path,
+      ErrorPtr* error);
+
+
+  base::DictionaryValue traits_;  // Trait definitions.
+  base::DictionaryValue components_;  // Component instances.
+  CommandQueue command_queue_;  // Command queue containing command instances.
+  std::vector<base::Callback<void()>> on_trait_changed_;
+  uint32_t next_command_id_{0};
+
+  DISALLOW_COPY_AND_ASSIGN(ComponentManager);
+};
+
+}  // namespace weave
+
+#endif  // LIBWEAVE_SRC_COMPONENT_MANAGER_H_
diff --git a/src/component_manager_unittest.cc b/src/component_manager_unittest.cc
new file mode 100644
index 0000000..ba6bc34
--- /dev/null
+++ b/src/component_manager_unittest.cc
@@ -0,0 +1,609 @@
+// Copyright 2015 The Weave 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 "src/component_manager.h"
+
+#include <map>
+
+#include <gtest/gtest.h>
+#include <weave/test/unittest_utils.h>
+
+#include "src/bind_lambda.h"
+#include "src/commands/schema_constants.h"
+
+namespace weave {
+
+using test::CreateDictionaryValue;
+
+namespace {
+
+bool HasTrait(const base::DictionaryValue& comp, const std::string& trait) {
+  const base::ListValue* list = nullptr;
+  if (!comp.GetList("traits", &list))
+    return false;
+  for (const base::Value* item : *list) {
+    std::string value;
+    if (item->GetAsString(&value) && value == trait)
+      return true;
+  }
+  return false;
+}
+
+}  // anonymous namespace
+
+TEST(ComponentManager, Empty) {
+  ComponentManager manager;
+  EXPECT_TRUE(manager.GetTraits().empty());
+  EXPECT_TRUE(manager.GetComponents().empty());
+}
+
+TEST(ComponentManager, LoadTraits) {
+  ComponentManager manager;
+  const char kTraits[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "integer"}}
+        }
+      },
+      "state": {
+        "property1": {"type": "boolean"}
+      }
+    },
+    "trait2": {
+      "state": {
+        "property2": {"type": "string"}
+      }
+    }
+  })";
+  auto json = CreateDictionaryValue(kTraits);
+  EXPECT_TRUE(manager.LoadTraits(*json, nullptr));
+  EXPECT_JSON_EQ(kTraits, manager.GetTraits());
+  EXPECT_TRUE(manager.GetComponents().empty());
+}
+
+TEST(ComponentManager, LoadTraitsDuplicateIdentical) {
+  ComponentManager manager;
+  const char kTraits1[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "integer"}}
+        }
+      },
+      "state": {
+        "property1": {"type": "boolean"}
+      }
+    },
+    "trait2": {
+      "state": {
+        "property2": {"type": "string"}
+      }
+    }
+  })";
+  auto json = CreateDictionaryValue(kTraits1);
+  EXPECT_TRUE(manager.LoadTraits(*json, nullptr));
+  const char kTraits2[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "integer"}}
+        }
+      },
+      "state": {
+        "property1": {"type": "boolean"}
+      }
+    },
+    "trait3": {
+      "state": {
+        "property3": {"type": "string"}
+      }
+    }
+  })";
+  json = CreateDictionaryValue(kTraits2);
+  EXPECT_TRUE(manager.LoadTraits(*json, nullptr));
+  const char kExpected[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "integer"}}
+        }
+      },
+      "state": {
+        "property1": {"type": "boolean"}
+      }
+    },
+    "trait2": {
+      "state": {
+        "property2": {"type": "string"}
+      }
+    },
+    "trait3": {
+      "state": {
+        "property3": {"type": "string"}
+      }
+    }
+  })";
+  EXPECT_JSON_EQ(kExpected, manager.GetTraits());
+}
+
+TEST(ComponentManager, LoadTraitsDuplicateOverride) {
+  ComponentManager manager;
+  const char kTraits1[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "integer"}}
+        }
+      },
+      "state": {
+        "property1": {"type": "boolean"}
+      }
+    },
+    "trait2": {
+      "state": {
+        "property2": {"type": "string"}
+      }
+    }
+  })";
+  auto json = CreateDictionaryValue(kTraits1);
+  EXPECT_TRUE(manager.LoadTraits(*json, nullptr));
+  const char kTraits2[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "string"}}
+        }
+      },
+      "state": {
+        "property1": {"type": "boolean"}
+      }
+    },
+    "trait3": {
+      "state": {
+        "property3": {"type": "string"}
+      }
+    }
+  })";
+  json = CreateDictionaryValue(kTraits2);
+  EXPECT_FALSE(manager.LoadTraits(*json, nullptr));
+}
+
+TEST(ComponentManager, LoadTraitsNotAnObject) {
+  ComponentManager manager;
+  const char kTraits1[] = R"({"trait1": 0})";
+  auto json = CreateDictionaryValue(kTraits1);
+  ErrorPtr error;
+  EXPECT_FALSE(manager.LoadTraits(*json, &error));
+  EXPECT_EQ(errors::commands::kTypeMismatch, error->GetCode());
+}
+
+TEST(ComponentManager, FindTraitDefinition) {
+  ComponentManager manager;
+  const char kTraits[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "integer"}}
+        }
+      },
+      "state": {
+        "property1": {"type": "boolean"}
+      }
+    },
+    "trait2": {
+      "state": {
+        "property2": {"type": "string"}
+      }
+    }
+  })";
+  auto json = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*json, nullptr));
+
+  const base::DictionaryValue* trait = manager.FindTraitDefinition("trait1");
+  ASSERT_NE(nullptr, trait);
+  const char kExpected1[] = R"({
+    "commands": {
+      "command1": {
+        "minimalRole": "user",
+        "parameters": {"height": {"type": "integer"}}
+      }
+    },
+    "state": {
+      "property1": {"type": "boolean"}
+    }
+  })";
+  EXPECT_JSON_EQ(kExpected1, *trait);
+
+  trait = manager.FindTraitDefinition("trait2");
+  ASSERT_NE(nullptr, trait);
+  const char kExpected2[] = R"({
+    "state": {
+      "property2": {"type": "string"}
+    }
+  })";
+  EXPECT_JSON_EQ(kExpected2, *trait);
+
+  EXPECT_EQ(nullptr, manager.FindTraitDefinition("trait3"));
+}
+
+TEST(ComponentManager, FindCommandDefinition) {
+  ComponentManager manager;
+  const char kTraits[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": {
+          "minimalRole": "user",
+          "parameters": {"height": {"type": "integer"}}
+        }
+      }
+    },
+    "trait2": {
+      "commands": {
+        "command1": {
+          "minimalRole": "manager"
+        },
+        "command2": {
+          "minimalRole": "owner"
+        }
+      }
+    }
+  })";
+  auto json = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*json, nullptr));
+
+  const auto* cmd_def = manager.FindCommandDefinition("trait1.command1");
+  ASSERT_NE(nullptr, cmd_def);
+  const char kExpected1[] = R"({
+    "minimalRole": "user",
+    "parameters": {"height": {"type": "integer"}}
+  })";
+  EXPECT_JSON_EQ(kExpected1, *cmd_def);
+
+  cmd_def = manager.FindCommandDefinition("trait2.command1");
+  ASSERT_NE(nullptr, cmd_def);
+  const char kExpected2[] = R"({
+    "minimalRole": "manager"
+  })";
+  EXPECT_JSON_EQ(kExpected2, *cmd_def);
+
+  cmd_def = manager.FindCommandDefinition("trait2.command2");
+  ASSERT_NE(nullptr, cmd_def);
+  const char kExpected3[] = R"({
+    "minimalRole": "owner"
+  })";
+  EXPECT_JSON_EQ(kExpected3, *cmd_def);
+
+  EXPECT_EQ(nullptr, manager.FindTraitDefinition("trait1.command2"));
+  EXPECT_EQ(nullptr, manager.FindTraitDefinition("trait3.command1"));
+  EXPECT_EQ(nullptr, manager.FindTraitDefinition("trait"));
+  EXPECT_EQ(nullptr, manager.FindTraitDefinition("trait1.command1.parameters"));
+}
+
+TEST(ComponentManager, GetMinimalRole) {
+  ComponentManager manager;
+  const char kTraits[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": { "minimalRole": "user" },
+        "command2": { "minimalRole": "viewer" }
+      }
+    },
+    "trait2": {
+      "commands": {
+        "command1": { "minimalRole": "manager" },
+        "command2": { "minimalRole": "owner" }
+      }
+    }
+  })";
+  auto json = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*json, nullptr));
+
+  UserRole role;
+  ASSERT_TRUE(manager.GetMinimalRole("trait1.command1", &role, nullptr));
+  EXPECT_EQ(UserRole::kUser, role);
+
+  ASSERT_TRUE(manager.GetMinimalRole("trait1.command2", &role, nullptr));
+  EXPECT_EQ(UserRole::kViewer, role);
+
+  ASSERT_TRUE(manager.GetMinimalRole("trait2.command1", &role, nullptr));
+  EXPECT_EQ(UserRole::kManager, role);
+
+  ASSERT_TRUE(manager.GetMinimalRole("trait2.command2", &role, nullptr));
+  EXPECT_EQ(UserRole::kOwner, role);
+
+  EXPECT_FALSE(manager.GetMinimalRole("trait1.command3", &role, nullptr));
+}
+
+TEST(ComponentManager, AddComponent) {
+  ComponentManager manager;
+  const char kTraits[] = R"({"trait1": {}, "trait2": {}, "trait3": {}})";
+  auto json = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*json, nullptr));
+  EXPECT_TRUE(manager.AddComponent("", "comp1", {"trait1", "trait2"}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("", "comp2", {"trait3"}, nullptr));
+  const char kExpected[] = R"({
+    "comp1": {
+      "traits": ["trait1", "trait2"]
+    },
+    "comp2": {
+      "traits": ["trait3"]
+    }
+  })";
+  EXPECT_JSON_EQ(kExpected, manager.GetComponents());
+
+  // 'trait4' is undefined, so can't add a component referring to it.
+  EXPECT_FALSE(manager.AddComponent("", "comp3", {"trait4"}, nullptr));
+}
+
+TEST(ComponentManager, AddSubComponent) {
+  ComponentManager manager;
+  EXPECT_TRUE(manager.AddComponent("", "comp1", {}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1", "comp2", {}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1", "comp3", {}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1.comp2", "comp4", {}, nullptr));
+  const char kExpected[] = R"({
+    "comp1": {
+      "traits": [],
+      "components": {
+        "comp2": {
+          "traits": [],
+          "components": {
+            "comp4": {
+              "traits": []
+            }
+          }
+        },
+        "comp3": {
+          "traits": []
+        }
+      }
+    }
+  })";
+  EXPECT_JSON_EQ(kExpected, manager.GetComponents());
+}
+
+TEST(ComponentManager, AddComponentArrayItem) {
+  ComponentManager manager;
+  const char kTraits[] = R"({"foo": {}, "bar": {}})";
+  auto json = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*json, nullptr));
+
+  EXPECT_TRUE(manager.AddComponent("", "comp1", {}, nullptr));
+  EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp2", {"foo"},
+                                            nullptr));
+  EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp2", {"bar"},
+                                            nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1.comp2[1]", "comp3", {}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1.comp2[1].comp3", "comp4", {},
+                                   nullptr));
+  const char kExpected[] = R"({
+    "comp1": {
+      "traits": [],
+      "components": {
+        "comp2": [
+          {
+            "traits": ["foo"]
+          },
+          {
+            "traits": ["bar"],
+            "components": {
+              "comp3": {
+                "traits": [],
+                "components": {
+                  "comp4": {
+                    "traits": []
+                  }
+                }
+              }
+            }
+          }
+        ]
+      }
+    }
+  })";
+  EXPECT_JSON_EQ(kExpected, manager.GetComponents());
+}
+
+TEST(ComponentManager, AddComponentExist) {
+  ComponentManager manager;
+  EXPECT_TRUE(manager.AddComponent("", "comp1", {}, nullptr));
+  EXPECT_FALSE(manager.AddComponent("", "comp1", {}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1", "comp2", {}, nullptr));
+  EXPECT_FALSE(manager.AddComponent("comp1", "comp2", {}, nullptr));
+}
+
+TEST(ComponentManager, AddComponentDoesNotExist) {
+  ComponentManager manager;
+  EXPECT_FALSE(manager.AddComponent("comp1", "comp2", {}, nullptr));
+}
+
+TEST(ComponentManager, FindComponent) {
+  ComponentManager manager;
+  const char kTraits[] = R"({"t1":{}, "t2":{}, "t3":{}, "t4":{}, "t5":{}})";
+  auto json = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*json, nullptr));
+
+  EXPECT_TRUE(manager.AddComponent("", "comp1", {"t1"}, nullptr));
+  EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp2", {"t2"}, nullptr));
+  EXPECT_TRUE(manager.AddComponentArrayItem("comp1", "comp2", {"t3"}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1.comp2[1]", "comp3", {"t4"}, nullptr));
+  EXPECT_TRUE(manager.AddComponent("comp1.comp2[1].comp3", "comp4", {"t5"},
+                                   nullptr));
+
+  const base::DictionaryValue* comp = manager.FindComponent("comp1", nullptr);
+  ASSERT_NE(nullptr, comp);
+  EXPECT_TRUE(HasTrait(*comp, "t1"));
+
+  comp = manager.FindComponent("comp1.comp2[0]", nullptr);
+  ASSERT_NE(nullptr, comp);
+  EXPECT_TRUE(HasTrait(*comp, "t2"));
+
+  comp = manager.FindComponent("comp1.comp2[1]", nullptr);
+  ASSERT_NE(nullptr, comp);
+  EXPECT_TRUE(HasTrait(*comp, "t3"));
+
+  comp = manager.FindComponent("comp1.comp2[1].comp3", nullptr);
+  ASSERT_NE(nullptr, comp);
+  EXPECT_TRUE(HasTrait(*comp, "t4"));
+
+  comp = manager.FindComponent("comp1.comp2[1].comp3.comp4", nullptr);
+  ASSERT_NE(nullptr, comp);
+  EXPECT_TRUE(HasTrait(*comp, "t5"));
+
+  // Some whitespaces don't hurt.
+  comp = manager.FindComponent(" comp1 . comp2 [  \t 1 ] .   comp3.comp4 ",
+                               nullptr);
+  EXPECT_NE(nullptr, comp);
+
+  // Now check some failure cases.
+  ErrorPtr error;
+  EXPECT_EQ(nullptr, manager.FindComponent("", &error));
+  EXPECT_NE(nullptr, error.get());
+  // 'comp2' doesn't exist:
+  EXPECT_EQ(nullptr, manager.FindComponent("comp2", nullptr));
+  // 'comp1.comp2' is an array, not a component:
+  EXPECT_EQ(nullptr, manager.FindComponent("comp1.comp2", nullptr));
+  // 'comp1.comp2[3]' doesn't exist:
+  EXPECT_EQ(nullptr, manager.FindComponent("comp1.comp2[3]", nullptr));
+  // Empty component names:
+  EXPECT_EQ(nullptr, manager.FindComponent(".comp2[1]", nullptr));
+  EXPECT_EQ(nullptr, manager.FindComponent("comp1.[1]", nullptr));
+  // Invalid array indices:
+  EXPECT_EQ(nullptr, manager.FindComponent("comp1.comp2[s]", nullptr));
+  EXPECT_EQ(nullptr, manager.FindComponent("comp1.comp2[-2]", nullptr));
+  EXPECT_EQ(nullptr, manager.FindComponent("comp1.comp2[1e1]", nullptr));
+  EXPECT_EQ(nullptr, manager.FindComponent("comp1.comp2[1", nullptr));
+}
+
+TEST(ComponentManager, AddCommand) {
+  ComponentManager manager;
+  const char kTraits[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": { "minimalRole": "user" },
+        "command2": { "minimalRole": "viewer" }
+      }
+    },
+    "trait2": {
+      "commands": {
+        "command1": { "minimalRole": "manager" },
+        "command2": { "minimalRole": "owner" }
+      }
+    }
+  })";
+  auto traits = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*traits, nullptr));
+  ASSERT_TRUE(manager.AddComponent("", "comp1", {"trait1"}, nullptr));
+  ASSERT_TRUE(manager.AddComponent("", "comp2", {"trait2"}, nullptr));
+
+  std::string id;
+  const char kCommand1[] = R"({
+    "name": "trait1.command1",
+    "component": "comp1",
+    "parameters": {}
+  })";
+  auto command1 = CreateDictionaryValue(kCommand1);
+  EXPECT_TRUE(manager.AddCommand(*command1, UserRole::kUser, &id, nullptr));
+  // Not enough access rights
+  EXPECT_FALSE(manager.AddCommand(*command1, UserRole::kViewer, &id, nullptr));
+
+  const char kCommand2[] = R"({
+    "name": "trait1.command3",
+    "component": "comp1",
+    "parameters": {}
+  })";
+  auto command2 = CreateDictionaryValue(kCommand2);
+  // trait1.command3 doesn't exist
+  EXPECT_FALSE(manager.AddCommand(*command2, UserRole::kOwner, &id, nullptr));
+
+  const char kCommand3[] = R"({
+    "name": "trait2.command1",
+    "component": "comp1",
+    "parameters": {}
+  })";
+  auto command3 = CreateDictionaryValue(kCommand3);
+  // Component comp1 doesn't have trait2.
+  EXPECT_FALSE(manager.AddCommand(*command3, UserRole::kOwner, &id, nullptr));
+
+  // No component specified, use the first top-level component (comp1)
+  const char kCommand4[] = R"({
+    "name": "trait1.command1",
+    "parameters": {}
+  })";
+  auto command4 = CreateDictionaryValue(kCommand4);
+  EXPECT_TRUE(manager.AddCommand(*command4, UserRole::kOwner, &id, nullptr));
+  auto cmd = manager.FindCommand(id);
+  ASSERT_NE(nullptr, cmd);
+  EXPECT_EQ("comp1", cmd->GetComponent());
+}
+
+TEST(ComponentManager, AddCommandHandler) {
+  ComponentManager manager;
+  const char kTraits[] = R"({
+    "trait1": {
+      "commands": {
+        "command1": { "minimalRole": "user" }
+      }
+    },
+    "trait2": {
+      "commands": {
+        "command2": { "minimalRole": "user" }
+      }
+    }
+  })";
+  auto traits = CreateDictionaryValue(kTraits);
+  ASSERT_TRUE(manager.LoadTraits(*traits, nullptr));
+  ASSERT_TRUE(manager.AddComponent("", "comp1", {"trait1"}, nullptr));
+  ASSERT_TRUE(manager.AddComponent("", "comp2", {"trait1", "trait2"}, nullptr));
+
+  std::string last_tags;
+  auto handler = [&last_tags](int tag, const std::weak_ptr<Command>& command) {
+    if (!last_tags.empty())
+      last_tags += ',';
+    last_tags += std::to_string(tag);
+  };
+
+  manager.AddCommandHandler("comp1", "trait1.command1", base::Bind(handler, 1));
+  manager.AddCommandHandler("comp2", "trait1.command1", base::Bind(handler, 2));
+  manager.AddCommandHandler("comp2", "trait2.command2", base::Bind(handler, 3));
+  EXPECT_TRUE(last_tags.empty());
+
+  const char kCommand1[] = R"({
+    "name": "trait1.command1",
+    "component": "comp1"
+  })";
+  auto command1 = CreateDictionaryValue(kCommand1);
+  EXPECT_TRUE(manager.AddCommand(*command1, UserRole::kUser, nullptr, nullptr));
+  EXPECT_EQ("1", last_tags);
+  last_tags.clear();
+
+  const char kCommand2[] = R"({
+    "name": "trait1.command1",
+    "component": "comp2"
+  })";
+  auto command2 = CreateDictionaryValue(kCommand2);
+  EXPECT_TRUE(manager.AddCommand(*command2, UserRole::kUser, nullptr, nullptr));
+  EXPECT_EQ("2", last_tags);
+  last_tags.clear();
+
+  const char kCommand3[] = R"({
+    "name": "trait2.command2",
+    "component": "comp2",
+    "parameters": {}
+  })";
+  auto command3 = CreateDictionaryValue(kCommand3);
+  EXPECT_TRUE(manager.AddCommand(*command3, UserRole::kUser, nullptr, nullptr));
+  EXPECT_EQ("3", last_tags);
+  last_tags.clear();
+}
+
+}  // namespace weave