Add the ability to query for particuar component and filter fields
When running local API /privet/v3/components add the ability to pass
extra parameters such as 'path' to specify a component path and 'filter'
to include only specified component properties.
BUG: 26159356
Change-Id: I7a5de42c2acff94dac2ad71874a0ade6f91fb064
Reviewed-on: https://weave-review.googlesource.com/1931
Reviewed-by: Alex Vakulenko <avakulenko@google.com>
diff --git a/src/privet/cloud_delegate.cc b/src/privet/cloud_delegate.cc
index 1747f7c..3c3f0bb 100644
--- a/src/privet/cloud_delegate.cc
+++ b/src/privet/cloud_delegate.cc
@@ -154,6 +154,11 @@
return component_manager_->GetComponents();
}
+ const base::DictionaryValue* FindComponent(const std::string& path,
+ ErrorPtr* error) const override {
+ return component_manager_->FindComponent(path, error);
+ }
+
const base::DictionaryValue& GetTraits() const override {
return component_manager_->GetTraits();
}
diff --git a/src/privet/cloud_delegate.h b/src/privet/cloud_delegate.h
index e80c39d..4aa9bcb 100644
--- a/src/privet/cloud_delegate.h
+++ b/src/privet/cloud_delegate.h
@@ -104,6 +104,11 @@
// Returns dictionary with component tree.
virtual const base::DictionaryValue& GetComponents() const = 0;
+ // Finds a component at the given path. Return nullptr in case of an error.
+ virtual const base::DictionaryValue* FindComponent(
+ const std::string& path,
+ ErrorPtr* error) const = 0;
+
// Returns dictionary with trait definitions.
virtual const base::DictionaryValue& GetTraits() const = 0;
diff --git a/src/privet/mock_delegates.h b/src/privet/mock_delegates.h
index 2065e9b..6762481 100644
--- a/src/privet/mock_delegates.h
+++ b/src/privet/mock_delegates.h
@@ -157,6 +157,9 @@
MOCK_CONST_METHOD0(GetLegacyState, const base::DictionaryValue&());
MOCK_CONST_METHOD0(GetLegacyCommandDef, const base::DictionaryValue&());
MOCK_CONST_METHOD0(GetComponents, const base::DictionaryValue&());
+ MOCK_CONST_METHOD2(FindComponent,
+ const base::DictionaryValue*(const std::string& path,
+ ErrorPtr* error));
MOCK_CONST_METHOD0(GetTraits, const base::DictionaryValue&());
MOCK_METHOD3(AddCommand,
void(const base::DictionaryValue&,
@@ -193,6 +196,7 @@
GetLegacyCommandDef()).WillRepeatedly(ReturnRef(test_dict_));
EXPECT_CALL(*this, GetTraits()).WillRepeatedly(ReturnRef(test_dict_));
EXPECT_CALL(*this, GetComponents()).WillRepeatedly(ReturnRef(test_dict_));
+ EXPECT_CALL(*this, FindComponent(_, _)).Times(0);
}
ConnectionState connection_state_{ConnectionState::kOnline};
diff --git a/src/privet/privet_handler.cc b/src/privet/privet_handler.cc
index 94061a5..86a2a85 100644
--- a/src/privet/privet_handler.cc
+++ b/src/privet/privet_handler.cc
@@ -106,6 +106,8 @@
const char kTraitsKey[] = "traits";
const char kComponentsKey[] = "components";
const char kCommandsIdKey[] = "id";
+const char kPathKey[] = "path";
+const char kFilterKey[] = "filter";
const char kStateFingerprintKey[] = "stateFingerprint";
const char kCommandsFingerprintKey[] = "commandsFingerprint";
@@ -319,6 +321,53 @@
return cloud.GetAnonymousMaxScope();
}
+// Forward-declaration.
+std::unique_ptr<base::DictionaryValue> CloneComponentTree(
+ const base::DictionaryValue& parent,
+ const std::set<std::string>& filter);
+
+// Clones a particular component JSON object in a manner similar to that of
+// DeepCopy(), except it includes only sub-objects specified in |filter| (if not
+// empty) and has special handling for "components" sub-dictionary.
+std::unique_ptr<base::DictionaryValue> CloneComponent(
+ const base::DictionaryValue& component,
+ const std::set<std::string>& filter) {
+ std::unique_ptr<base::DictionaryValue> clone{new base::DictionaryValue};
+ for (base::DictionaryValue::Iterator it(component); !it.IsAtEnd();
+ it.Advance()) {
+ if (filter.empty() || filter.find(it.key()) != filter.end()) {
+ if (it.key() == kComponentsKey) {
+ // Handle "components" separately as we need to recursively clone
+ // sub-components.
+ const base::DictionaryValue* sub_components = nullptr;
+ CHECK(it.value().GetAsDictionary(&sub_components));
+ clone->SetWithoutPathExpansion(
+ it.key(), CloneComponentTree(*sub_components, filter).release());
+ } else {
+ clone->SetWithoutPathExpansion(it.key(), it.value().DeepCopy());
+ }
+ }
+ }
+ return clone;
+}
+
+// Clones a dictionary containing a bunch of component JSON objects in a manner
+// similar to that of DeepCopy(). Calls CloneComponent() on each instance of
+// the component sub-object.
+std::unique_ptr<base::DictionaryValue> CloneComponentTree(
+ const base::DictionaryValue& parent,
+ const std::set<std::string>& filter) {
+ std::unique_ptr<base::DictionaryValue> clone{new base::DictionaryValue};
+ for (base::DictionaryValue::Iterator it(parent); !it.IsAtEnd();
+ it.Advance()) {
+ const base::DictionaryValue* component = nullptr;
+ CHECK(it.value().GetAsDictionary(&component));
+ clone->SetWithoutPathExpansion(
+ it.key(), CloneComponent(*component, filter).release());
+ }
+ return clone;
+}
+
} // namespace
std::vector<std::string> PrivetHandler::GetHttpPaths() const {
@@ -806,8 +855,34 @@
void PrivetHandler::HandleComponents(const base::DictionaryValue& input,
const UserInfo& user_info,
const RequestCallback& callback) {
+ std::string path;
+ std::set<std::string> filter;
+ std::unique_ptr<base::DictionaryValue> components;
+
+ input.GetString(kPathKey, &path);
+ const base::ListValue* filter_items = nullptr;
+ if (input.GetList(kFilterKey, &filter_items)) {
+ for (const base::Value* value : *filter_items) {
+ std::string filter_item;
+ if (value->GetAsString(&filter_item))
+ filter.insert(filter_item);
+ }
+ }
+ const base::DictionaryValue* component = nullptr;
+ if (!path.empty()) {
+ ErrorPtr error;
+ component = cloud_->FindComponent(path, &error);
+ if (!component)
+ return ReturnError(*error, callback);
+ components.reset(new base::DictionaryValue);
+ // Get the last element of the path and use it as a dictionary key here.
+ auto parts = Split(path, ".", true, false);
+ components->Set(parts.back(), CloneComponent(*component, filter).release());
+ } else {
+ components = CloneComponentTree(cloud_->GetComponents(), filter);
+ }
base::DictionaryValue output;
- output.Set(kComponentsKey, cloud_->GetComponents().DeepCopy());
+ output.Set(kComponentsKey, components.release());
output.SetString(kFingerprintKey, std::to_string(components_fingerprint_));
callback.Run(http::kOk, output);
diff --git a/src/privet/privet_handler_unittest.cc b/src/privet/privet_handler_unittest.cc
index 12182d9..20856f7 100644
--- a/src/privet/privet_handler_unittest.cc
+++ b/src/privet/privet_handler_unittest.cc
@@ -643,6 +643,145 @@
HandleRequest("/privet/v3/components", "{}"));
}
+TEST_F(PrivetHandlerSetupTest, ComponentsWithFiltersAndPaths) {
+ const char kComponents[] = R"({
+ "comp1": {
+ "traits": ["a", "b"],
+ "state": {
+ "a" : {
+ "prop": 1
+ }
+ },
+ "components": {
+ "comp2": {
+ "traits": ["c"],
+ "components": {
+ "comp4": {
+ "traits": ["d"]
+ }
+ }
+ },
+ "comp3": {
+ "traits": ["e"]
+ }
+ }
+ }
+ })";
+ base::DictionaryValue components;
+ LoadTestJson(kComponents, &components);
+ EXPECT_CALL(cloud_, FindComponent(_, _)).WillRepeatedly(Return(nullptr));
+ EXPECT_CALL(cloud_, GetComponents()).WillRepeatedly(ReturnRef(components));
+ const char kExpected1[] = R"({
+ "components": {
+ "comp1": {
+ "state": {
+ "a" : {
+ "prop": 1
+ }
+ }
+ }
+ },
+ "fingerprint": "1"
+ })";
+ EXPECT_JSON_EQ(kExpected1, HandleRequest("/privet/v3/components",
+ "{'filter':['state']}"));
+
+ const char kExpected2[] = R"({
+ "components": {
+ "comp1": {
+ "traits": ["a", "b"]
+ }
+ },
+ "fingerprint": "1"
+ })";
+ EXPECT_JSON_EQ(kExpected2, HandleRequest("/privet/v3/components",
+ "{'filter':['traits']}"));
+
+ const char kExpected3[] = R"({
+ "components": {
+ "comp1": {
+ "components": {
+ "comp2": {
+ "components": {
+ "comp4": {}
+ }
+ },
+ "comp3": {}
+ }
+ }
+ },
+ "fingerprint": "1"
+ })";
+ EXPECT_JSON_EQ(kExpected3, HandleRequest("/privet/v3/components",
+ "{'filter':['components']}"));
+
+ const char kExpected4[] = R"({
+ "components": {
+ "comp1": {
+ "traits": ["a", "b"],
+ "state": {
+ "a" : {
+ "prop": 1
+ }
+ },
+ "components": {
+ "comp2": {
+ "traits": ["c"],
+ "components": {
+ "comp4": {
+ "traits": ["d"]
+ }
+ }
+ },
+ "comp3": {
+ "traits": ["e"]
+ }
+ }
+ }
+ },
+ "fingerprint": "1"
+ })";
+ EXPECT_JSON_EQ(kExpected4,
+ HandleRequest("/privet/v3/components",
+ "{'filter':['traits', 'components', 'state']}"));
+
+ const base::DictionaryValue* comp2 = nullptr;
+ ASSERT_TRUE(components.GetDictionary("comp1.components.comp2", &comp2));
+ EXPECT_CALL(cloud_, FindComponent("comp1.comp2", _))
+ .WillOnce(Return(comp2));
+
+ const char kExpected5[] = R"({
+ "components": {
+ "comp2": {
+ "traits": ["c"],
+ "components": {
+ "comp4": {
+ "traits": ["d"]
+ }
+ }
+ }
+ },
+ "fingerprint": "1"
+ })";
+ EXPECT_JSON_EQ(kExpected5, HandleRequest(
+ "/privet/v3/components",
+ "{'path':'comp1.comp2', 'filter':['traits', 'components']}"));
+
+ auto error_handler = [](ErrorPtr* error) -> const base::DictionaryValue* {
+ Error::AddTo(error, FROM_HERE, errors::kDomain, "componentNotFound", "");
+ return nullptr;
+ };
+ EXPECT_CALL(cloud_, FindComponent("comp7", _))
+ .WillOnce(WithArgs<1>(Invoke(error_handler)));
+
+ EXPECT_PRED2(
+ IsEqualError,
+ CodeWithReason(500, "componentNotFound"),
+ HandleRequest(
+ "/privet/v3/components",
+ "{'path':'comp7', 'filter':['traits', 'components']}"));
+}
+
TEST_F(PrivetHandlerSetupTest, CommandsExecute) {
const char kInput[] = "{'name': 'test'}";
base::DictionaryValue command;