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;