Repository: googleapis/google-cloud-python

Issue:
[Firestore] FirestoreClient.list_collections() is missing, but is in DocumentReference.
FirestoreClient.list_collections() is missing, but it does exist as part of DocumentReference.



Discussion:
The [`ListCollectionIds` API reference](https://cloud.google.com/firestore/docs/reference/rpc/google.firestore.v1beta1#google.firestore.v1beta1.Firestore.ListCollectionIds) describes the method:

> Lists all the collection IDs underneath a document.

I can see no reason to add such a method to the client:  it is pointless without first having a `DocuementReference`, at which point you just use its method.
By hacking around, Firestore apparently accepts the database path as `parent` passed to the `ListCollectionIds` API:

```python
>>> from google.cloud.firestore_v1beta1 import Client
>>> client = Client()
>>> api = client._firestore_api
>>> list(api.list_collection_ids(parent=client._database_string))
['users']
```

So, we could reasonable expose `Client.collections()` returning that iterator.

Resolving patch:
diff --git a/firestore/google/cloud/firestore_v1beta1/client.py b/firestore/google/cloud/firestore_v1beta1/client.py
--- a/firestore/google/cloud/firestore_v1beta1/client.py
+++ b/firestore/google/cloud/firestore_v1beta1/client.py
@@ -332,6 +332,19 @@ def get_all(self, references, field_paths=None, transaction=None):
         for get_doc_response in response_iterator:
             yield _parse_batch_get(get_doc_response, reference_map, self)
 
+    def collections(self):
+        """List top-level collections of the client's database.
+
+        Returns:
+            Sequence[~.firestore_v1beta1.collection.CollectionReference]:
+                iterator of subcollections of the current document.
+        """
+        iterator = self._firestore_api.list_collection_ids(
+            self._database_string, metadata=self._rpc_metadata)
+        iterator.client = self
+        iterator.item_to_value = _item_to_collection_ref
+        return iterator
+
     def batch(self):
         """Get a batch instance from this client.
 
@@ -567,3 +580,14 @@ def _get_doc_mask(field_paths):
         return None
     else:
         return types.DocumentMask(field_paths=field_paths)
+
+
+def _item_to_collection_ref(iterator, item):
+    """Convert collection ID to collection ref.
+
+    Args:
+        iterator (google.api_core.page_iterator.GRPCIterator):
+            iterator response
+        item (str): ID of the collection
+    """
+    return iterator.client.collection(item)


Test patch:
diff --git a/firestore/tests/unit/test_client.py b/firestore/tests/unit/test_client.py
--- a/firestore/tests/unit/test_client.py
+++ b/firestore/tests/unit/test_client.py
@@ -226,6 +226,44 @@ def test_write_bad_arg(self):
         extra = '{!r} was provided'.format('spinach')
         self.assertEqual(exc_info.exception.args, (_BAD_OPTION_ERR, extra))
 
+    def test_collections(self):
+        from google.api_core.page_iterator import Iterator
+        from google.api_core.page_iterator import Page
+        from google.cloud.firestore_v1beta1.collection import (
+            CollectionReference)
+
+        collection_ids = ['users', 'projects']
+        client = self._make_default_one()
+        firestore_api = mock.Mock(spec=['list_collection_ids'])
+        client._firestore_api_internal = firestore_api
+
+        class _Iterator(Iterator):
+
+            def __init__(self, pages):
+                super(_Iterator, self).__init__(client=None)
+                self._pages = pages
+
+            def _next_page(self):
+                if self._pages:
+                    page, self._pages = self._pages[0], self._pages[1:]
+                    return Page(self, page, self.item_to_value)
+
+        iterator = _Iterator(pages=[collection_ids])
+        firestore_api.list_collection_ids.return_value = iterator
+
+        collections = list(client.collections())
+
+        self.assertEqual(len(collections), len(collection_ids))
+        for collection, collection_id in zip(collections, collection_ids):
+            self.assertIsInstance(collection, CollectionReference)
+            self.assertEqual(collection.parent, None)
+            self.assertEqual(collection.id, collection_id)
+
+        firestore_api.list_collection_ids.assert_called_once_with(
+            client._database_string,
+            metadata=client._rpc_metadata,
+        )
+
     def _get_all_helper(self, client, references, document_pbs, **kwargs):
         # Create a minimal fake GAPIC with a dummy response.
         firestore_api = mock.Mock(spec=['batch_get_documents'])