Repository: googleapis/python-bigquery

Issue:
refactor AccessEntry to use `_properties` pattern
 **Is your feature request related to a problem? Please describe.**

Often there is a private preview feature (or just a new feature that we haven't implemented yet) and we want to provide our customers a workaround to be able to send a resource as represented by JSON. Then we could have provided a workaround to the user in https://github.com/googleapis/python-bigquery/issues/1064 like the following:

```
entry = AccessEntry.from_api_repr({
  "role": "READER",
  "dataset": {
    "projectId": "project-id",
    "datasetId": "dataset_id",
  }
})
```

or even

```
entry = AccessEntry("READER")
entry._properties["dataset"] = {
    "projectId": "project-id",
    "datasetId": "dataset_id",
}
```

and been confident that it would send the correct values when making the API request.

Likewise, one could read values from the API like `entry._properties["dataset"]` when checking values from the API.

 **Describe the solution you'd like**

The way we handle `entity_type` and `entity_id` is reminiscent of `ExternalConfig.options`, which I refactored in https://github.com/googleapis/python-bigquery/pull/994. It was clever at the time when there were only a few external data formats, but has diverged far the actual API representation.

`AccessEntry` should have separate properties for `view`, `routine`, `dataset`, etc. These could accept and return relevant types (e.g. TableReference) as well.

Existing `entity_type` and `entity_id` should be made optional in the constructor, but in the same order so that backwards compatibility is maintained.

 **Describe alternatives you've considered**

Leaving the class as-is should continue to work, but it's a significant risk IMO.

 **Additional context**

As identified in https://github.com/googleapis/python-bigquery/pull/1075

See similar work in https://github.com/googleapis/python-bigquery/pull/994


Discussion:
Eventually, I'd like to see `to_api_repr` become `return copy.deepcopy(self._properties)` as we have in our other resource objects, but I proposed a stepping stone in #1075 where we start `to_api_repr` with `resource = copy.deepcopy(self._properties)` here:

https://github.com/googleapis/python-bigquery/blob/3314dfbed62488503dc41b11e403a672fcf71048/google/cloud/bigquery/dataset.py#L217
Assigning to @steffnay , as I believe she's already done some work on this through #1075 

Resolving patch:
diff --git a/google/cloud/bigquery/dataset.py b/google/cloud/bigquery/dataset.py
--- a/google/cloud/bigquery/dataset.py
+++ b/google/cloud/bigquery/dataset.py
@@ -17,16 +17,19 @@
 from __future__ import absolute_import
 
 import copy
-from typing import Dict, Any
+
+import typing
 
 import google.cloud._helpers  # type: ignore
 
 from google.cloud.bigquery import _helpers
 from google.cloud.bigquery.model import ModelReference
-from google.cloud.bigquery.routine import RoutineReference
-from google.cloud.bigquery.table import TableReference
+from google.cloud.bigquery.routine import Routine, RoutineReference
+from google.cloud.bigquery.table import Table, TableReference
 from google.cloud.bigquery.encryption_configuration import EncryptionConfiguration
 
+from typing import Optional, List, Dict, Any, Union
+
 
 def _get_table_reference(self, table_id: str) -> TableReference:
     """Constructs a TableReference.
@@ -75,173 +78,6 @@ def _get_routine_reference(self, routine_id):
     )
 
 
-class AccessEntry(object):
-    """Represents grant of an access role to an entity.
-
-    An entry must have exactly one of the allowed
-    :class:`google.cloud.bigquery.enums.EntityTypes`. If anything but ``view``, ``routine``,
-    or ``dataset`` are set, a ``role`` is also required. ``role`` is omitted for ``view``,
-    ``routine``, ``dataset``, because they are always read-only.
-
-    See https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets.
-
-    Args:
-        role (str):
-            Role granted to the entity. The following string values are
-            supported: `'READER'`, `'WRITER'`, `'OWNER'`. It may also be
-            :data:`None` if the ``entity_type`` is ``view``, ``routine``, or ``dataset``.
-
-        entity_type (str):
-            Type of entity being granted the role. See
-            :class:`google.cloud.bigquery.enums.EntityTypes` for supported types.
-
-        entity_id (Union[str, Dict[str, str]]):
-            If the ``entity_type`` is not 'view', 'routine', or 'dataset', the
-            ``entity_id`` is the ``str`` ID of the entity being granted the role. If
-            the ``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict``
-            representing the view or routine from a different dataset to grant access
-            to in the following format for views::
-
-                {
-                    'projectId': string,
-                    'datasetId': string,
-                    'tableId': string
-                }
-
-            For routines::
-
-                {
-                    'projectId': string,
-                    'datasetId': string,
-                    'routineId': string
-                }
-
-            If the ``entity_type`` is 'dataset', the ``entity_id`` is a ``dict`` that includes
-            a 'dataset' field with a ``dict`` representing the dataset and a 'target_types'
-            field with a ``str`` value of the dataset's resource type::
-
-                {
-                    'dataset': {
-                        'projectId': string,
-                        'datasetId': string,
-                    },
-                    'target_types: 'VIEWS'
-                }
-
-    Raises:
-        ValueError:
-            If a ``view``, ``routine``, or ``dataset`` has ``role`` set, or a non ``view``,
-            non ``routine``, and non ``dataset`` **does not** have a ``role`` set.
-
-    Examples:
-        >>> entry = AccessEntry('OWNER', 'userByEmail', 'user@example.com')
-
-        >>> view = {
-        ...     'projectId': 'my-project',
-        ...     'datasetId': 'my_dataset',
-        ...     'tableId': 'my_table'
-        ... }
-        >>> entry = AccessEntry(None, 'view', view)
-    """
-
-    def __init__(self, role=None, entity_type=None, entity_id=None) -> None:
-        self._properties: Dict[str, Any] = {}
-        if entity_type in ("view", "routine", "dataset"):
-            if role is not None:
-                raise ValueError(
-                    "Role must be None for a %r. Received "
-                    "role: %r" % (entity_type, role)
-                )
-        else:
-            if role is None:
-                raise ValueError(
-                    "Role must be set for entity " "type %r" % (entity_type,)
-                )
-        self._role = role
-        self._entity_type = entity_type
-        self._entity_id = entity_id
-
-    @property
-    def role(self):
-        """str: The role of the entry."""
-        return self._role
-
-    @property
-    def entity_type(self):
-        """str: The entity_type of the entry."""
-        return self._entity_type
-
-    @property
-    def entity_id(self):
-        """str: The entity_id of the entry."""
-        return self._entity_id
-
-    def __eq__(self, other):
-        if not isinstance(other, AccessEntry):
-            return NotImplemented
-        return self._key() == other._key()
-
-    def __ne__(self, other):
-        return not self == other
-
-    def __repr__(self):
-        return "<AccessEntry: role=%s, %s=%s>" % (
-            self._role,
-            self._entity_type,
-            self._entity_id,
-        )
-
-    def _key(self):
-        """A tuple key that uniquely describes this field.
-        Used to compute this instance's hashcode and evaluate equality.
-        Returns:
-            Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
-        """
-        return (self._role, self._entity_type, self._entity_id)
-
-    def __hash__(self):
-        return hash(self._key())
-
-    def to_api_repr(self):
-        """Construct the API resource representation of this access entry
-
-        Returns:
-            Dict[str, object]: Access entry represented as an API resource
-        """
-        resource = copy.deepcopy(self._properties)
-        resource[self._entity_type] = self._entity_id
-        if self._role is not None:
-            resource["role"] = self._role
-        return resource
-
-    @classmethod
-    def from_api_repr(cls, resource: dict) -> "AccessEntry":
-        """Factory: construct an access entry given its API representation
-
-        Args:
-            resource (Dict[str, object]):
-                Access entry resource representation returned from the API
-
-        Returns:
-            google.cloud.bigquery.dataset.AccessEntry:
-                Access entry parsed from ``resource``.
-
-        Raises:
-            ValueError:
-                If the resource has more keys than ``role`` and one additional
-                key.
-        """
-        entry = resource.copy()
-        role = entry.pop("role", None)
-        entity_type, entity_id = entry.popitem()
-        if len(entry) != 0:
-            raise ValueError("Entry has unexpected keys remaining.", entry)
-
-        config = cls(role, entity_type, entity_id)
-        config._properties = copy.deepcopy(resource)
-        return config
-
-
 class DatasetReference(object):
     """DatasetReferences are pointers to datasets.
 
@@ -383,6 +219,291 @@ def __repr__(self):
         return "DatasetReference{}".format(self._key())
 
 
+class AccessEntry(object):
+    """Represents grant of an access role to an entity.
+
+    An entry must have exactly one of the allowed
+    :class:`google.cloud.bigquery.enums.EntityTypes`. If anything but ``view``, ``routine``,
+    or ``dataset`` are set, a ``role`` is also required. ``role`` is omitted for ``view``,
+    ``routine``, ``dataset``, because they are always read-only.
+
+    See https://cloud.google.com/bigquery/docs/reference/rest/v2/datasets.
+
+    Args:
+        role:
+            Role granted to the entity. The following string values are
+            supported: `'READER'`, `'WRITER'`, `'OWNER'`. It may also be
+            :data:`None` if the ``entity_type`` is ``view``, ``routine``, or ``dataset``.
+
+        entity_type:
+            Type of entity being granted the role. See
+            :class:`google.cloud.bigquery.enums.EntityTypes` for supported types.
+
+        entity_id:
+            If the ``entity_type`` is not 'view', 'routine', or 'dataset', the
+            ``entity_id`` is the ``str`` ID of the entity being granted the role. If
+            the ``entity_type`` is 'view' or 'routine', the ``entity_id`` is a ``dict``
+            representing the view or routine from a different dataset to grant access
+            to in the following format for views::
+
+                {
+                    'projectId': string,
+                    'datasetId': string,
+                    'tableId': string
+                }
+
+            For routines::
+
+                {
+                    'projectId': string,
+                    'datasetId': string,
+                    'routineId': string
+                }
+
+            If the ``entity_type`` is 'dataset', the ``entity_id`` is a ``dict`` that includes
+            a 'dataset' field with a ``dict`` representing the dataset and a 'target_types'
+            field with a ``str`` value of the dataset's resource type::
+
+                {
+                    'dataset': {
+                        'projectId': string,
+                        'datasetId': string,
+                    },
+                    'target_types: 'VIEWS'
+                }
+
+    Raises:
+        ValueError:
+            If a ``view``, ``routine``, or ``dataset`` has ``role`` set, or a non ``view``,
+            non ``routine``, and non ``dataset`` **does not** have a ``role`` set.
+
+    Examples:
+        >>> entry = AccessEntry('OWNER', 'userByEmail', 'user@example.com')
+
+        >>> view = {
+        ...     'projectId': 'my-project',
+        ...     'datasetId': 'my_dataset',
+        ...     'tableId': 'my_table'
+        ... }
+        >>> entry = AccessEntry(None, 'view', view)
+    """
+
+    def __init__(
+        self,
+        role: Optional[str] = None,
+        entity_type: Optional[str] = None,
+        entity_id: Optional[Union[Dict[str, Any], str]] = None,
+    ):
+        self._properties = {}
+        if entity_type is not None:
+            self._properties[entity_type] = entity_id
+        self._properties["role"] = role
+        self._entity_type = entity_type
+
+    @property
+    def role(self) -> Optional[str]:
+        """The role of the entry."""
+        return typing.cast(Optional[str], self._properties.get("role"))
+
+    @role.setter
+    def role(self, value):
+        self._properties["role"] = value
+
+    @property
+    def dataset(self) -> Optional[DatasetReference]:
+        """API resource representation of a dataset reference."""
+        value = _helpers._get_sub_prop(self._properties, ["dataset", "dataset"])
+        return DatasetReference.from_api_repr(value) if value else None
+
+    @dataset.setter
+    def dataset(self, value):
+        if self.role is not None:
+            raise ValueError(
+                "Role must be None for a dataset. Current " "role: %r" % (self.role)
+            )
+
+        if isinstance(value, str):
+            value = DatasetReference.from_string(value).to_api_repr()
+
+        if isinstance(value, (Dataset, DatasetListItem)):
+            value = value.reference.to_api_repr()
+
+        _helpers._set_sub_prop(self._properties, ["dataset", "dataset"], value)
+        _helpers._set_sub_prop(
+            self._properties,
+            ["dataset", "targetTypes"],
+            self._properties.get("targetTypes"),
+        )
+
+    @property
+    def dataset_target_types(self) -> Optional[List[str]]:
+        """Which resources that the dataset in this entry applies to."""
+        return typing.cast(
+            Optional[List[str]],
+            _helpers._get_sub_prop(self._properties, ["dataset", "targetTypes"]),
+        )
+
+    @dataset_target_types.setter
+    def dataset_target_types(self, value):
+        self._properties.setdefault("dataset", {})
+        _helpers._set_sub_prop(self._properties, ["dataset", "targetTypes"], value)
+
+    @property
+    def routine(self) -> Optional[RoutineReference]:
+        """API resource representation of a routine reference."""
+        value = typing.cast(Optional[Dict], self._properties.get("routine"))
+        return RoutineReference.from_api_repr(value) if value else None
+
+    @routine.setter
+    def routine(self, value):
+        if self.role is not None:
+            raise ValueError(
+                "Role must be None for a routine. Current " "role: %r" % (self.role)
+            )
+
+        if isinstance(value, str):
+            value = RoutineReference.from_string(value).to_api_repr()
+
+        if isinstance(value, RoutineReference):
+            value = value.to_api_repr()
+
+        if isinstance(value, Routine):
+            value = value.reference.to_api_repr()
+
+        self._properties["routine"] = value
+
+    @property
+    def view(self) -> Optional[TableReference]:
+        """API resource representation of a view reference."""
+        value = typing.cast(Optional[Dict], self._properties.get("view"))
+        return TableReference.from_api_repr(value) if value else None
+
+    @view.setter
+    def view(self, value):
+        if self.role is not None:
+            raise ValueError(
+                "Role must be None for a view. Current " "role: %r" % (self.role)
+            )
+
+        if isinstance(value, str):
+            value = TableReference.from_string(value).to_api_repr()
+
+        if isinstance(value, TableReference):
+            value = value.to_api_repr()
+
+        if isinstance(value, Table):
+            value = value.reference.to_api_repr()
+
+        self._properties["view"] = value
+
+    @property
+    def group_by_email(self) -> Optional[str]:
+        """An email address of a Google Group to grant access to."""
+        return typing.cast(Optional[str], self._properties.get("groupByEmail"))
+
+    @group_by_email.setter
+    def group_by_email(self, value):
+        self._properties["groupByEmail"] = value
+
+    @property
+    def user_by_email(self) -> Optional[str]:
+        """An email address of a user to grant access to."""
+        return typing.cast(Optional[str], self._properties.get("userByEmail"))
+
+    @user_by_email.setter
+    def user_by_email(self, value):
+        self._properties["userByEmail"] = value
+
+    @property
+    def domain(self) -> Optional[str]:
+        """A domain to grant access to."""
+        return typing.cast(Optional[str], self._properties.get("domain"))
+
+    @domain.setter
+    def domain(self, value):
+        self._properties["domain"] = value
+
+    @property
+    def special_group(self) -> Optional[str]:
+        """A special group to grant access to."""
+        return typing.cast(Optional[str], self._properties.get("specialGroup"))
+
+    @special_group.setter
+    def special_group(self, value):
+        self._properties["specialGroup"] = value
+
+    @property
+    def entity_type(self) -> Optional[str]:
+        """The entity_type of the entry."""
+        return self._entity_type
+
+    @property
+    def entity_id(self) -> Optional[Union[Dict[str, Any], str]]:
+        """The entity_id of the entry."""
+        return self._properties.get(self._entity_type) if self._entity_type else None
+
+    def __eq__(self, other):
+        if not isinstance(other, AccessEntry):
+            return NotImplemented
+        return self._key() == other._key()
+
+    def __ne__(self, other):
+        return not self == other
+
+    def __repr__(self):
+
+        return f"<AccessEntry: role={self.role}, {self._entity_type}={self.entity_id}>"
+
+    def _key(self):
+        """A tuple key that uniquely describes this field.
+        Used to compute this instance's hashcode and evaluate equality.
+        Returns:
+            Tuple: The contents of this :class:`~google.cloud.bigquery.dataset.AccessEntry`.
+        """
+        properties = self._properties.copy()
+        prop_tup = tuple(sorted(properties.items()))
+        return (self.role, self._entity_type, self.entity_id, prop_tup)
+
+    def __hash__(self):
+        return hash(self._key())
+
+    def to_api_repr(self):
+        """Construct the API resource representation of this access entry
+
+        Returns:
+            Dict[str, object]: Access entry represented as an API resource
+        """
+        resource = copy.deepcopy(self._properties)
+        return resource
+
+    @classmethod
+    def from_api_repr(cls, resource: dict) -> "AccessEntry":
+        """Factory: construct an access entry given its API representation
+
+        Args:
+            resource (Dict[str, object]):
+                Access entry resource representation returned from the API
+
+        Returns:
+            google.cloud.bigquery.dataset.AccessEntry:
+                Access entry parsed from ``resource``.
+
+        Raises:
+            ValueError:
+                If the resource has more keys than ``role`` and one additional
+                key.
+        """
+        entry = resource.copy()
+        role = entry.pop("role", None)
+        entity_type, entity_id = entry.popitem()
+        if len(entry) != 0:
+            raise ValueError("Entry has unexpected keys remaining.", entry)
+
+        config = cls(role, entity_type, entity_id)
+        config._properties = copy.deepcopy(resource)
+        return config
+
+
 class Dataset(object):
     """Datasets are containers for tables.
 


Test patch:
diff --git a/tests/unit/test_create_dataset.py b/tests/unit/test_create_dataset.py
--- a/tests/unit/test_create_dataset.py
+++ b/tests/unit/test_create_dataset.py
@@ -109,7 +109,10 @@ def test_create_dataset_w_attrs(client, PROJECT, DS_ID):
             "friendlyName": FRIENDLY_NAME,
             "location": LOCATION,
             "defaultTableExpirationMs": "3600",
-            "access": [{"role": "OWNER", "userByEmail": USER_EMAIL}, {"view": VIEW}],
+            "access": [
+                {"role": "OWNER", "userByEmail": USER_EMAIL},
+                {"view": VIEW, "role": None},
+            ],
             "labels": LABELS,
         },
         timeout=DEFAULT_TIMEOUT,
diff --git a/tests/unit/test_dataset.py b/tests/unit/test_dataset.py
--- a/tests/unit/test_dataset.py
+++ b/tests/unit/test_dataset.py
@@ -15,14 +15,20 @@
 import unittest
 
 import mock
+from google.cloud.bigquery.routine.routine import Routine, RoutineReference
 import pytest
+from google.cloud.bigquery.dataset import (
+    AccessEntry,
+    Dataset,
+    DatasetReference,
+    Table,
+    TableReference,
+)
 
 
 class TestAccessEntry(unittest.TestCase):
     @staticmethod
     def _get_target_class():
-        from google.cloud.bigquery.dataset import AccessEntry
-
         return AccessEntry
 
     def _make_one(self, *args, **kw):
@@ -34,16 +40,6 @@ def test_ctor_defaults(self):
         self.assertEqual(entry.entity_type, "userByEmail")
         self.assertEqual(entry.entity_id, "phred@example.com")
 
-    def test_ctor_bad_entity_type(self):
-        with self.assertRaises(ValueError):
-            self._make_one(None, "unknown", None)
-
-    def test_ctor_view_with_role(self):
-        role = "READER"
-        entity_type = "view"
-        with self.assertRaises(ValueError):
-            self._make_one(role, entity_type, None)
-
     def test_ctor_view_success(self):
         role = None
         entity_type = "view"
@@ -53,12 +49,6 @@ def test_ctor_view_success(self):
         self.assertEqual(entry.entity_type, entity_type)
         self.assertEqual(entry.entity_id, entity_id)
 
-    def test_ctor_routine_with_role(self):
-        role = "READER"
-        entity_type = "routine"
-        with self.assertRaises(ValueError):
-            self._make_one(role, entity_type, None)
-
     def test_ctor_routine_success(self):
         role = None
         entity_type = "routine"
@@ -68,12 +58,6 @@ def test_ctor_routine_success(self):
         self.assertEqual(entry.entity_type, entity_type)
         self.assertEqual(entry.entity_id, entity_id)
 
-    def test_ctor_nonview_without_role(self):
-        role = None
-        entity_type = "userByEmail"
-        with self.assertRaises(ValueError):
-            self._make_one(role, entity_type, None)
-
     def test___eq___role_mismatch(self):
         entry = self._make_one("OWNER", "userByEmail", "phred@example.com")
         other = self._make_one("WRITER", "userByEmail", "phred@example.com")
@@ -127,7 +111,7 @@ def test_to_api_repr_view(self):
         }
         entry = self._make_one(None, "view", view)
         resource = entry.to_api_repr()
-        exp_resource = {"view": view}
+        exp_resource = {"view": view, "role": None}
         self.assertEqual(resource, exp_resource)
 
     def test_to_api_repr_routine(self):
@@ -136,9 +120,10 @@ def test_to_api_repr_routine(self):
             "datasetId": "my_dataset",
             "routineId": "my_routine",
         }
+
         entry = self._make_one(None, "routine", routine)
         resource = entry.to_api_repr()
-        exp_resource = {"routine": routine}
+        exp_resource = {"routine": routine, "role": None}
         self.assertEqual(resource, exp_resource)
 
     def test_to_api_repr_dataset(self):
@@ -148,21 +133,9 @@ def test_to_api_repr_dataset(self):
         }
         entry = self._make_one(None, "dataset", dataset)
         resource = entry.to_api_repr()
-        exp_resource = {"dataset": dataset}
+        exp_resource = {"dataset": dataset, "role": None}
         self.assertEqual(resource, exp_resource)
 
-    def test_to_api_w_incorrect_role(self):
-        dataset = {
-            "dataset": {
-                "projectId": "my-project",
-                "datasetId": "my_dataset",
-                "tableId": "my_table",
-            },
-            "target_type": "VIEW",
-        }
-        with self.assertRaises(ValueError):
-            self._make_one("READER", "dataset", dataset)
-
     def test_from_api_repr(self):
         resource = {"role": "OWNER", "userByEmail": "salmon@example.com"}
         entry = self._get_target_class().from_api_repr(resource)
@@ -198,6 +171,311 @@ def test_from_api_repr_entries_w_extra_keys(self):
         with self.assertRaises(ValueError):
             self._get_target_class().from_api_repr(resource)
 
+    def test_view_getter_setter(self):
+        view = {
+            "projectId": "my_project",
+            "datasetId": "my_dataset",
+            "tableId": "my_table",
+        }
+        view_ref = TableReference.from_api_repr(view)
+        entry = self._make_one(None)
+        entry.view = view
+        resource = entry.to_api_repr()
+        exp_resource = {"view": view, "role": None}
+        self.assertEqual(entry.view, view_ref)
+        self.assertEqual(resource, exp_resource)
+
+    def test_view_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.view, None)
+
+    def test_view_getter_setter_string(self):
+        project = "my_project"
+        dataset = "my_dataset"
+        table = "my_table"
+        view = {
+            "projectId": project,
+            "datasetId": dataset,
+            "tableId": table,
+        }
+        entry = self._make_one(None)
+        entry.view = f"{project}.{dataset}.{table}"
+        resource = entry.to_api_repr()
+        exp_resource = {"view": view, "role": None}
+        self.assertEqual(resource, exp_resource)
+
+    def test_view_getter_setter_table(self):
+        project = "my_project"
+        dataset = "my_dataset"
+        table = "my_table"
+        view = {
+            "projectId": project,
+            "datasetId": dataset,
+            "tableId": table,
+        }
+        view_ref = Table.from_string(f"{project}.{dataset}.{table}")
+        entry = self._make_one(None)
+        entry.view = view_ref
+        resource = entry.to_api_repr()
+        exp_resource = {"view": view, "role": None}
+        self.assertEqual(resource, exp_resource)
+
+    def test_view_getter_setter_table_ref(self):
+        project = "my_project"
+        dataset = "my_dataset"
+        table = "my_table"
+        view = {
+            "projectId": project,
+            "datasetId": dataset,
+            "tableId": table,
+        }
+        view_ref = TableReference.from_string(f"{project}.{dataset}.{table}")
+        entry = self._make_one(None)
+        entry.view = view_ref
+        resource = entry.to_api_repr()
+        exp_resource = {"view": view, "role": None}
+        self.assertEqual(resource, exp_resource)
+
+    def test_view_getter_setter_incorrect_role(self):
+        view = {
+            "projectId": "my_project",
+            "datasetId": "my_dataset",
+            "tableId": "my_table",
+        }
+        view_ref = TableReference.from_api_repr(view)
+        entry = self._make_one("READER")
+        with self.assertRaises(ValueError):
+            entry.view = view_ref
+
+    def test_dataset_getter_setter(self):
+        dataset = {"projectId": "my-project", "datasetId": "my_dataset"}
+        entry = self._make_one(None)
+        entry.dataset = dataset
+        resource = entry.to_api_repr()
+        exp_resource = {
+            "dataset": {"dataset": dataset, "targetTypes": None},
+            "role": None,
+        }
+        dataset_ref = DatasetReference.from_api_repr(dataset)
+        prop = entry.dataset
+        self.assertEqual(resource, exp_resource)
+        self.assertEqual(prop, dataset_ref)
+
+    def test_dataset_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.dataset, None)
+
+    def test_dataset_getter_setter_string(self):
+        project = "my-project"
+        dataset_id = "my_dataset"
+        dataset = {
+            "projectId": project,
+            "datasetId": dataset_id,
+        }
+        entry = self._make_one(None)
+        string_ref = f"{project}.{dataset_id}"
+        entry.dataset = string_ref
+        resource = entry.to_api_repr()
+        exp_resource = {
+            "dataset": {"dataset": dataset, "targetTypes": None},
+            "role": None,
+        }
+        self.assertEqual(resource, exp_resource)
+
+    def test_dataset_getter_setter_dataset_ref(self):
+        project = "my-project"
+        dataset_id = "my_dataset"
+        dataset_ref = DatasetReference(project, dataset_id)
+        entry = self._make_one(None)
+        entry.dataset = dataset_ref
+        resource = entry.to_api_repr()
+        exp_resource = {
+            "dataset": {"dataset": dataset_ref, "targetTypes": None},
+            "role": None,
+        }
+        self.assertEqual(resource, exp_resource)
+
+    def test_dataset_getter_setter_dataset(self):
+        project = "my-project"
+        dataset_id = "my_dataset"
+        dataset_repr = {
+            "projectId": project,
+            "datasetId": dataset_id,
+        }
+        dataset = Dataset(f"{project}.{dataset_id}")
+        entry = self._make_one(None)
+        entry.dataset = dataset
+        resource = entry.to_api_repr()
+        exp_resource = {
+            "role": None,
+            "dataset": {"dataset": dataset_repr, "targetTypes": None},
+        }
+        self.assertEqual(resource, exp_resource)
+
+    def test_dataset_getter_setter_incorrect_role(self):
+        dataset = {"dataset": {"projectId": "my-project", "datasetId": "my_dataset"}}
+        entry = self._make_one("READER")
+        with self.assertRaises(ValueError):
+            entry.dataset = dataset
+
+    def test_routine_getter_setter(self):
+        routine = {
+            "projectId": "my-project",
+            "datasetId": "my_dataset",
+            "routineId": "my_routine",
+        }
+        entry = self._make_one(None)
+        entry.routine = routine
+        resource = entry.to_api_repr()
+        exp_resource = {"routine": routine, "role": None}
+        self.assertEqual(resource, exp_resource)
+
+    def test_routine_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.routine, None)
+
+    def test_routine_getter_setter_string(self):
+        project = "my-project"
+        dataset_id = "my_dataset"
+        routine_id = "my_routine"
+        routine = {
+            "projectId": project,
+            "datasetId": dataset_id,
+            "routineId": routine_id,
+        }
+        entry = self._make_one(None)
+        entry.routine = f"{project}.{dataset_id}.{routine_id}"
+        resource = entry.to_api_repr()
+        exp_resource = {
+            "routine": routine,
+            "role": None,
+        }
+        self.assertEqual(resource, exp_resource)
+
+    def test_routine_getter_setter_routine_ref(self):
+        routine = {
+            "projectId": "my-project",
+            "datasetId": "my_dataset",
+            "routineId": "my_routine",
+        }
+        entry = self._make_one(None)
+        entry.routine = RoutineReference.from_api_repr(routine)
+        resource = entry.to_api_repr()
+        exp_resource = {
+            "routine": routine,
+            "role": None,
+        }
+        self.assertEqual(resource, exp_resource)
+
+    def test_routine_getter_setter_routine(self):
+        routine = {
+            "projectId": "my-project",
+            "datasetId": "my_dataset",
+            "routineId": "my_routine",
+        }
+        routine_ref = RoutineReference.from_api_repr(routine)
+        entry = self._make_one(None)
+        entry.routine = Routine(routine_ref)
+        resource = entry.to_api_repr()
+        exp_resource = {
+            "routine": routine,
+            "role": None,
+        }
+        self.assertEqual(entry.routine, routine_ref)
+        self.assertEqual(resource, exp_resource)
+
+    def test_routine_getter_setter_incorrect_role(self):
+        routine = {
+            "projectId": "my-project",
+            "datasetId": "my_dataset",
+            "routineId": "my_routine",
+        }
+        entry = self._make_one("READER")
+        with self.assertRaises(ValueError):
+            entry.routine = routine
+
+    def test_group_by_email_getter_setter(self):
+        email = "cloud-developer-relations@google.com"
+        entry = self._make_one(None)
+        entry.group_by_email = email
+        resource = entry.to_api_repr()
+        exp_resource = {"groupByEmail": email, "role": None}
+        self.assertEqual(entry.group_by_email, email)
+        self.assertEqual(resource, exp_resource)
+
+    def test_group_by_email_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.group_by_email, None)
+
+    def test_user_by_email_getter_setter(self):
+        email = "cloud-developer-relations@google.com"
+        entry = self._make_one(None)
+        entry.user_by_email = email
+        resource = entry.to_api_repr()
+        exp_resource = {"userByEmail": email, "role": None}
+        self.assertEqual(entry.user_by_email, email)
+        self.assertEqual(resource, exp_resource)
+
+    def test_user_by_email_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.user_by_email, None)
+
+    def test_domain_setter(self):
+        domain = "my_domain"
+        entry = self._make_one(None)
+        entry.domain = domain
+        resource = entry.to_api_repr()
+        exp_resource = {"domain": domain, "role": None}
+        self.assertEqual(entry.domain, domain)
+        self.assertEqual(resource, exp_resource)
+
+    def test_domain_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.domain, None)
+
+    def test_special_group_getter_setter(self):
+        special_group = "my_special_group"
+        entry = self._make_one(None)
+        entry.special_group = special_group
+        resource = entry.to_api_repr()
+        exp_resource = {"specialGroup": special_group, "role": None}
+        self.assertEqual(entry.special_group, special_group)
+        self.assertEqual(resource, exp_resource)
+
+    def test_special_group_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.special_group, None)
+
+    def test_role_getter_setter(self):
+        role = "READER"
+        entry = self._make_one(None)
+        entry.role = role
+        resource = entry.to_api_repr()
+        exp_resource = {"role": role}
+        self.assertEqual(resource, exp_resource)
+
+    def test_role_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.role, None)
+
+    def test_dataset_target_types_getter_setter(self):
+        target_types = ["VIEWS"]
+        entry = self._make_one(None)
+        entry.dataset_target_types = target_types
+        self.assertEqual(entry.dataset_target_types, target_types)
+
+    def test_dataset_target_types_getter_setter_none(self):
+        entry = self._make_one(None)
+        self.assertEqual(entry.dataset_target_types, None)
+
+    def test_dataset_target_types_getter_setter_w_dataset(self):
+        dataset = {"projectId": "my-project", "datasetId": "my_dataset"}
+        target_types = ["VIEWS"]
+        entry = self._make_one(None)
+        entry.dataset = dataset
+        entry.dataset_target_types = target_types
+        self.assertEqual(entry.dataset_target_types, target_types)
+
 
 class TestDatasetReference(unittest.TestCase):
     @staticmethod