Browse Source

Adapt internal gateway (#15)

Casper van der Wel 1 year ago
parent
commit
f59dd18648

+ 4 - 2
CHANGES.md

@@ -1,10 +1,12 @@
 # Changelog of clean-python
 
 
-0.4.4 (unreleased)
+0.5.0 (unreleased)
 ------------------
 
-- Nothing changed yet.
+- Adapt `InternalGateway` so that it derives from `Gateway`.
+
+- Renamed the old `InternalGateway` to `TypedInternalGateway`.
 
 
 0.4.3 (2023-09-11)

+ 1 - 0
clean_python/base/infrastructure/__init__.py

@@ -1,3 +1,4 @@
 from .in_memory_gateway import *  # NOQA
 from .internal_gateway import *  # NOQA
 from .tmpdir_provider import *  # NOQA
+from .typed_internal_gateway import *  # NOQA

+ 21 - 23
clean_python/base/infrastructure/internal_gateway.py

@@ -1,6 +1,7 @@
 # (c) Nelen & Schuurmans
 from abc import abstractmethod
 from abc import abstractproperty
+from datetime import datetime
 from typing import Generic
 from typing import List
 from typing import Optional
@@ -10,50 +11,44 @@ from clean_python.base.application.manage import Manage
 from clean_python.base.domain import BadRequest
 from clean_python.base.domain import DoesNotExist
 from clean_python.base.domain import Filter
+from clean_python.base.domain import Gateway
+from clean_python.base.domain import Id
 from clean_python.base.domain import Json
 from clean_python.base.domain import PageOptions
 from clean_python.base.domain import RootEntity
-from clean_python.base.domain import ValueObject
 
 __all__ = ["InternalGateway"]
 
 
-E = TypeVar("E", bound=RootEntity)  # External
-T = TypeVar("T", bound=ValueObject)  # Internal
+T = TypeVar("T", bound=RootEntity)  # External
 
 
-# don't subclass Gateway; Gateway makes Json objects
-class InternalGateway(Generic[E, T]):
+class InternalGateway(Gateway, Generic[T]):
     @abstractproperty
-    def manage(self) -> Manage[E]:
+    def manage(self) -> Manage[T]:
         raise NotImplementedError()
 
     @abstractmethod
-    def _map(self, obj: E) -> T:
+    def to_internal(self, obj: T) -> Json:
         raise NotImplementedError()
 
-    async def get(self, id: int) -> Optional[T]:
-        try:
-            result = await self.manage.retrieve(id)
-        except DoesNotExist:
-            return None
-        else:
-            return self._map(result)
+    def to_external(self, values: Json) -> Json:
+        return values
 
     async def filter(
         self, filters: List[Filter], params: Optional[PageOptions] = None
-    ) -> List[T]:
+    ) -> List[Json]:
         page = await self.manage.filter(filters, params)
-        return [self._map(x) for x in page.items]
+        return [self.to_internal(x) for x in page.items]
 
-    async def add(self, item: T) -> T:
+    async def add(self, item: Json) -> Json:
         try:
-            created = await self.manage.create(item.model_dump())
+            created = await self.manage.create(self.to_external(item))
         except BadRequest as e:
             raise ValueError(e)
-        return self._map(created)
+        return self.to_internal(created)
 
-    async def remove(self, id) -> bool:
+    async def remove(self, id: Id) -> bool:
         return await self.manage.destroy(id)
 
     async def count(self, filters: List[Filter]) -> int:
@@ -62,8 +57,11 @@ class InternalGateway(Generic[E, T]):
     async def exists(self, filters: List[Filter]) -> bool:
         return await self.manage.exists(filters)
 
-    async def update(self, values: Json) -> T:
-        values = values.copy()
+    async def update(
+        self, item: Json, if_unmodified_since: Optional[datetime] = None
+    ) -> Json:
+        assert if_unmodified_since is None  # unsupported
+        values = self.to_external(item)
         id_ = values.pop("id", None)
         if id_ is None:
             raise DoesNotExist("item", id_)
@@ -71,4 +69,4 @@ class InternalGateway(Generic[E, T]):
             updated = await self.manage.update(id_, values)
         except BadRequest as e:
             raise ValueError(e)
-        return self._map(updated)
+        return self.to_internal(updated)

+ 74 - 0
clean_python/base/infrastructure/typed_internal_gateway.py

@@ -0,0 +1,74 @@
+# (c) Nelen & Schuurmans
+from abc import abstractmethod
+from abc import abstractproperty
+from typing import Generic
+from typing import List
+from typing import Optional
+from typing import TypeVar
+
+from clean_python.base.application.manage import Manage
+from clean_python.base.domain import BadRequest
+from clean_python.base.domain import DoesNotExist
+from clean_python.base.domain import Filter
+from clean_python.base.domain import Json
+from clean_python.base.domain import PageOptions
+from clean_python.base.domain import RootEntity
+from clean_python.base.domain import ValueObject
+
+__all__ = ["TypedInternalGateway"]
+
+
+E = TypeVar("E", bound=RootEntity)  # External
+T = TypeVar("T", bound=ValueObject)  # Internal
+
+
+# don't subclass Gateway; Gateway makes Json objects
+class TypedInternalGateway(Generic[E, T]):
+    @abstractproperty
+    def manage(self) -> Manage[E]:
+        raise NotImplementedError()
+
+    @abstractmethod
+    def _map(self, obj: E) -> T:
+        raise NotImplementedError()
+
+    async def get(self, id: int) -> Optional[T]:
+        try:
+            result = await self.manage.retrieve(id)
+        except DoesNotExist:
+            return None
+        else:
+            return self._map(result)
+
+    async def filter(
+        self, filters: List[Filter], params: Optional[PageOptions] = None
+    ) -> List[T]:
+        page = await self.manage.filter(filters, params)
+        return [self._map(x) for x in page.items]
+
+    async def add(self, item: T) -> T:
+        try:
+            created = await self.manage.create(item.model_dump())
+        except BadRequest as e:
+            raise ValueError(e)
+        return self._map(created)
+
+    async def remove(self, id) -> bool:
+        return await self.manage.destroy(id)
+
+    async def count(self, filters: List[Filter]) -> int:
+        return await self.manage.count(filters)
+
+    async def exists(self, filters: List[Filter]) -> bool:
+        return await self.manage.exists(filters)
+
+    async def update(self, values: Json) -> T:
+        values = values.copy()
+        id_ = values.pop("id", None)
+        if id_ is None:
+            raise DoesNotExist("item", id_)
+        try:
+            updated = await self.manage.update(id_, values)
+        except BadRequest as e:
+            raise ValueError(e)
+        return self._map(updated)

+ 10 - 19
tests/test_internal_gateway.py

@@ -1,5 +1,3 @@
-from typing import cast
-
 import pytest
 from pydantic import Field
 
@@ -13,7 +11,6 @@ from clean_python import Json
 from clean_python import Manage
 from clean_python import Repository
 from clean_python import RootEntity
-from clean_python import ValueObject
 
 
 # domain - other module
@@ -36,16 +33,10 @@ class ManageUser(Manage[User]):
         return await self.repo.update(id, values)
 
 
-# domain - this module
-class UserObj(ValueObject):
-    id: int
-    name: str
-
-
 # infrastructure - this module
 
 
-class UserGateway(InternalGateway[User, UserObj]):
+class UserGateway(InternalGateway[User]):
     def __init__(self, manage: ManageUser):
         self._manage = manage
 
@@ -53,8 +44,8 @@ class UserGateway(InternalGateway[User, UserObj]):
     def manage(self) -> ManageUser:
         return self._manage
 
-    def _map(self, obj: User) -> UserObj:
-        return UserObj(id=cast(int, obj.id), name=obj.name)
+    def to_internal(self, obj: User) -> Json:
+        return dict(id=obj.id, name=obj.name)
 
 
 @pytest.fixture
@@ -67,23 +58,23 @@ async def test_get_not_existing(internal_gateway: UserGateway):
 
 
 async def test_add(internal_gateway: UserGateway):
-    actual = await internal_gateway.add(UserObj(id=12, name="foo"))
+    actual = await internal_gateway.add(dict(id=12, name="foo"))
 
-    assert actual == UserObj(id=12, name="foo")
+    assert actual == dict(id=12, name="foo")
 
 
 @pytest.fixture
 async def internal_gateway_with_record(internal_gateway):
-    await internal_gateway.add(UserObj(id=12, name="foo"))
+    await internal_gateway.add(dict(id=12, name="foo"))
     return internal_gateway
 
 
 async def test_get(internal_gateway_with_record):
-    assert await internal_gateway_with_record.get(12) == UserObj(id=12, name="foo")
+    assert await internal_gateway_with_record.get(12) == dict(id=12, name="foo")
 
 
 async def test_filter(internal_gateway_with_record: UserGateway):
-    assert await internal_gateway_with_record.filter([]) == [UserObj(id=12, name="foo")]
+    assert await internal_gateway_with_record.filter([]) == [dict(id=12, name="foo")]
 
 
 async def test_filter_2(internal_gateway_with_record: UserGateway):
@@ -107,7 +98,7 @@ async def test_add_bad_request(internal_gateway: UserGateway):
     # a 'bad request' should be reraised as a ValueError; errors in gateways
     # are an internal affair.
     with pytest.raises(ValueError):
-        await internal_gateway.add(UserObj(id=12, name=""))
+        await internal_gateway.add(dict(id=12, name=""))
 
 
 async def test_count(internal_gateway_with_record: UserGateway):
@@ -134,7 +125,7 @@ async def test_exists_2(internal_gateway_with_record: UserGateway):
 async def test_update(internal_gateway_with_record):
     updated = await internal_gateway_with_record.update({"id": 12, "name": "bar"})
 
-    assert updated == UserObj(id=12, name="bar")
+    assert updated == dict(id=12, name="bar")
 
 
 @pytest.mark.parametrize(

+ 158 - 0
tests/test_typed_internal_gateway.py

@@ -0,0 +1,158 @@
+from typing import cast
+
+import pytest
+from pydantic import Field
+
+from clean_python import Conflict
+from clean_python import DoesNotExist
+from clean_python import Filter
+from clean_python import Id
+from clean_python import InMemoryGateway
+from clean_python import Json
+from clean_python import Manage
+from clean_python import Repository
+from clean_python import RootEntity
+from clean_python import TypedInternalGateway
+from clean_python import ValueObject
+
+
+# domain - other module
+class User(RootEntity):
+    name: str = Field(min_length=1)
+
+
+class UserRepository(Repository[User]):
+    pass
+
+
+# application - other module
+class ManageUser(Manage[User]):
+    def __init__(self):
+        self.repo = UserRepository(gateway=InMemoryGateway([]))
+
+    async def update(self, id: Id, values: Json) -> User:
+        if values.get("name") == "conflict":
+            raise Conflict()
+        return await self.repo.update(id, values)
+
+
+# domain - this module
+class UserObj(ValueObject):
+    id: int
+    name: str
+
+
+# infrastructure - this module
+
+
+class UserGateway(TypedInternalGateway[User, UserObj]):
+    def __init__(self, manage: ManageUser):
+        self._manage = manage
+
+    @property
+    def manage(self) -> ManageUser:
+        return self._manage
+
+    def _map(self, obj: User) -> UserObj:
+        return UserObj(id=cast(int, obj.id), name=obj.name)
+
+
+@pytest.fixture
+def internal_gateway():
+    return UserGateway(manage=ManageUser())
+
+
+async def test_get_not_existing(internal_gateway: UserGateway):
+    assert await internal_gateway.get(1) is None
+
+
+async def test_add(internal_gateway: UserGateway):
+    actual = await internal_gateway.add(UserObj(id=12, name="foo"))
+
+    assert actual == UserObj(id=12, name="foo")
+
+
+@pytest.fixture
+async def internal_gateway_with_record(internal_gateway):
+    await internal_gateway.add(UserObj(id=12, name="foo"))
+    return internal_gateway
+
+
+async def test_get(internal_gateway_with_record):
+    assert await internal_gateway_with_record.get(12) == UserObj(id=12, name="foo")
+
+
+async def test_filter(internal_gateway_with_record: UserGateway):
+    assert await internal_gateway_with_record.filter([]) == [UserObj(id=12, name="foo")]
+
+
+async def test_filter_2(internal_gateway_with_record: UserGateway):
+    assert (
+        await internal_gateway_with_record.filter([Filter(field="id", values=[1])])
+        == []
+    )
+
+
+async def test_remove(internal_gateway_with_record: UserGateway):
+    assert await internal_gateway_with_record.remove(12)
+
+    assert internal_gateway_with_record.manage.repo.gateway.data == {}
+
+
+async def test_remove_does_not_exist(internal_gateway: UserGateway):
+    assert not await internal_gateway.remove(12)
+
+
+async def test_add_bad_request(internal_gateway: UserGateway):
+    # a 'bad request' should be reraised as a ValueError; errors in gateways
+    # are an internal affair.
+    with pytest.raises(ValueError):
+        await internal_gateway.add(UserObj(id=12, name=""))
+
+
+async def test_count(internal_gateway_with_record: UserGateway):
+    assert await internal_gateway_with_record.count([]) == 1
+
+
+async def test_count_2(internal_gateway_with_record: UserGateway):
+    assert (
+        await internal_gateway_with_record.count([Filter(field="id", values=[1])]) == 0
+    )
+
+
+async def test_exists(internal_gateway_with_record: UserGateway):
+    assert await internal_gateway_with_record.exists([]) is True
+
+
+async def test_exists_2(internal_gateway_with_record: UserGateway):
+    assert (
+        await internal_gateway_with_record.exists([Filter(field="id", values=[1])])
+        is False
+    )
+
+
+async def test_update(internal_gateway_with_record):
+    updated = await internal_gateway_with_record.update({"id": 12, "name": "bar"})
+
+    assert updated == UserObj(id=12, name="bar")
+
+
+@pytest.mark.parametrize(
+    "values", [{"id": 12, "name": "bar"}, {"id": None, "name": "bar"}, {"name": "bar"}]
+)
+async def test_update_does_not_exist(internal_gateway, values):
+    with pytest.raises(DoesNotExist):
+        assert await internal_gateway.update(values)
+
+
+async def test_update_bad_request(internal_gateway_with_record):
+    # a 'bad request' should be reraised as a ValueError; errors in gateways
+    # are an internal affair.
+    with pytest.raises(ValueError):
+        assert await internal_gateway_with_record.update({"id": 12, "name": ""})
+
+
+async def test_update_conflict(internal_gateway_with_record):
+    # a 'conflict' should bubble through the internal gateway
+    with pytest.raises(Conflict):
+        assert await internal_gateway_with_record.update({"id": 12, "name": "conflict"})