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;