From fad06c45f83e81e3beee0c9de340abaf863d92f3 Mon Sep 17 00:00:00 2001 From: Sergey Vanyushkin Date: Tue, 3 Sep 2024 22:37:00 +0000 Subject: [PATCH] Add Domain entity --- .../application/contracts/gateways.py | 8 ----- .../application/contracts/requests.py | 7 ++++ .../application/contracts/responses.py | 4 +-- .../application/usecases/menu/add_menu.py | 27 +++++++++++++++ .../usecases/menu/get_all_menus.py | 17 +++++++--- src/fastfood_two/domain/core/entity.py | 11 ++++++ src/fastfood_two/domain/core/error.py | 8 +++++ src/fastfood_two/domain/core/value_obj.py | 6 ++++ src/fastfood_two/domain/menu/__init__.py | 0 src/fastfood_two/domain/menu/error.py | 9 +++++ src/fastfood_two/domain/menu/gateway.py | 21 ++++++++++++ src/fastfood_two/domain/menu/menu_entity.py | 34 +++++++++++++++++++ .../infrastructure/menu_gateway.py | 20 ++++++++--- .../pg_storage/mappers/menu_mapper.py | 10 +++--- .../pg_storage/models/__init__.py | 4 +-- .../pg_storage/models/menu_model.py | 12 +++---- .../fastapi_backend/dependencies.py | 2 +- .../fastapi_backend/depends/gateways.py | 2 +- .../fastapi_backend/depends/usecases.py | 7 +++- .../fastapi_backend/pdmodels/__init__.py | 0 .../fastapi_backend/pdmodels/menu.py | 6 ++++ .../fastapi_backend/routers/menu.py | 22 ++++++++++-- 22 files changed, 199 insertions(+), 38 deletions(-) delete mode 100644 src/fastfood_two/application/contracts/gateways.py create mode 100644 src/fastfood_two/application/usecases/menu/add_menu.py create mode 100644 src/fastfood_two/domain/core/entity.py create mode 100644 src/fastfood_two/domain/core/error.py create mode 100644 src/fastfood_two/domain/core/value_obj.py create mode 100644 src/fastfood_two/domain/menu/__init__.py create mode 100644 src/fastfood_two/domain/menu/error.py create mode 100644 src/fastfood_two/domain/menu/gateway.py create mode 100644 src/fastfood_two/domain/menu/menu_entity.py create mode 100644 src/fastfood_two/presentation/fastapi_backend/pdmodels/__init__.py create mode 100644 src/fastfood_two/presentation/fastapi_backend/pdmodels/menu.py diff --git a/src/fastfood_two/application/contracts/gateways.py b/src/fastfood_two/application/contracts/gateways.py deleted file mode 100644 index 9fa6474..0000000 --- a/src/fastfood_two/application/contracts/gateways.py +++ /dev/null @@ -1,8 +0,0 @@ -from typing import Protocol - -from fastfood_two.application.contracts.responses import MenuResponse - - -class MenuGateway(Protocol): - async def get_all_menus(self) -> list[MenuResponse]: - raise NotImplementedError diff --git a/src/fastfood_two/application/contracts/requests.py b/src/fastfood_two/application/contracts/requests.py index e69de29..9f6fff8 100644 --- a/src/fastfood_two/application/contracts/requests.py +++ b/src/fastfood_two/application/contracts/requests.py @@ -0,0 +1,7 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class AddMenuDTO: + title: str + description: str | None diff --git a/src/fastfood_two/application/contracts/responses.py b/src/fastfood_two/application/contracts/responses.py index e63050c..3d4e74b 100644 --- a/src/fastfood_two/application/contracts/responses.py +++ b/src/fastfood_two/application/contracts/responses.py @@ -3,7 +3,7 @@ from uuid import UUID @dataclass(frozen=True) -class MenuResponse: +class MenuDTO: id: UUID title: str - description: str + description: str | None diff --git a/src/fastfood_two/application/usecases/menu/add_menu.py b/src/fastfood_two/application/usecases/menu/add_menu.py new file mode 100644 index 0000000..4bc35ba --- /dev/null +++ b/src/fastfood_two/application/usecases/menu/add_menu.py @@ -0,0 +1,27 @@ +from uuid import uuid4 + +from fastfood_two.application.abstractions.interactor import Interactor +from fastfood_two.application.contracts.requests import AddMenuDTO +from fastfood_two.application.contracts.responses import MenuDTO +from fastfood_two.domain.menu.gateway import MenuGateway +from fastfood_two.domain.menu.menu_entity import Description, Menu, MenuId, Title + + +class AddMenu(Interactor[AddMenuDTO, MenuDTO]): + def __init__(self, gateway: MenuGateway) -> None: + self._menu_gateway = gateway + + async def __call__(self, request: AddMenuDTO) -> MenuDTO: + + menu = await self._menu_gateway.insert_menu( + menu=Menu( + id=MenuId(uuid4()), + title=Title(request.title), + description=Description(request.description), + ) + ) + return MenuDTO( + id=menu.id.value, + title=menu.title.value, + description=menu.description.value, + ) diff --git a/src/fastfood_two/application/usecases/menu/get_all_menus.py b/src/fastfood_two/application/usecases/menu/get_all_menus.py index 38b66d1..f67f088 100644 --- a/src/fastfood_two/application/usecases/menu/get_all_menus.py +++ b/src/fastfood_two/application/usecases/menu/get_all_menus.py @@ -1,12 +1,19 @@ from fastfood_two.application.abstractions.interactor import Interactor -from fastfood_two.application.contracts.gateways import MenuGateway -from fastfood_two.application.contracts.responses import MenuResponse +from fastfood_two.application.contracts.responses import MenuDTO +from fastfood_two.domain.menu.gateway import MenuGateway -class GetAllMenus(Interactor[None, list[MenuResponse]]): +class GetAllMenus(Interactor[None, list[MenuDTO]]): def __init__(self, gateway: MenuGateway) -> None: self._menu_gateway = gateway - async def __call__(self, request=None) -> list[MenuResponse]: + async def __call__(self, request=None) -> list[MenuDTO]: menus = await self._menu_gateway.get_all_menus() - return menus + return [ + MenuDTO( + id=menu.id.value, + title=menu.title.value, + description=menu.description.value, + ) + for menu in menus + ] diff --git a/src/fastfood_two/domain/core/entity.py b/src/fastfood_two/domain/core/entity.py new file mode 100644 index 0000000..2070027 --- /dev/null +++ b/src/fastfood_two/domain/core/entity.py @@ -0,0 +1,11 @@ +from dataclasses import dataclass +from typing import Generic, TypeVar + +from fastfood_two.domain.core.value_obj import DomainValueObject + +EntityId = TypeVar("EntityId", bound=DomainValueObject) + + +@dataclass +class DomainEntity(Generic[EntityId]): + id: EntityId diff --git a/src/fastfood_two/domain/core/error.py b/src/fastfood_two/domain/core/error.py new file mode 100644 index 0000000..77379f9 --- /dev/null +++ b/src/fastfood_two/domain/core/error.py @@ -0,0 +1,8 @@ +class DomainError(Exception): + def __init__(self, message: str, *args: object) -> None: + self.message = message + super().__init__(*args) + + +class DomainValidationError(DomainError): + pass diff --git a/src/fastfood_two/domain/core/value_obj.py b/src/fastfood_two/domain/core/value_obj.py new file mode 100644 index 0000000..96c1411 --- /dev/null +++ b/src/fastfood_two/domain/core/value_obj.py @@ -0,0 +1,6 @@ +from dataclasses import dataclass + + +@dataclass(frozen=True) +class DomainValueObject: + pass diff --git a/src/fastfood_two/domain/menu/__init__.py b/src/fastfood_two/domain/menu/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastfood_two/domain/menu/error.py b/src/fastfood_two/domain/menu/error.py new file mode 100644 index 0000000..32fa66a --- /dev/null +++ b/src/fastfood_two/domain/menu/error.py @@ -0,0 +1,9 @@ +from fastfood_two.domain.core.error import DomainError + + +class MenuNotFoundError(DomainError): + pass + + +class MenuDataValidationError(DomainError): + pass diff --git a/src/fastfood_two/domain/menu/gateway.py b/src/fastfood_two/domain/menu/gateway.py new file mode 100644 index 0000000..ffb763a --- /dev/null +++ b/src/fastfood_two/domain/menu/gateway.py @@ -0,0 +1,21 @@ +from typing import Protocol +from uuid import UUID + +from fastfood_two.domain.menu.menu_entity import Menu + + +class MenuGateway(Protocol): + async def get_menu_by_id(self, id: UUID) -> Menu | None: + raise NotImplementedError + + async def insert_menu(self, menu: Menu) -> Menu: + raise NotImplementedError + + async def get_all_menus(self) -> list[Menu]: + raise NotImplementedError + + async def update_menu(self, updated_menu: Menu) -> Menu | None: + raise NotImplementedError + + async def delete_menu(self, id: UUID) -> None: + raise NotImplementedError diff --git a/src/fastfood_two/domain/menu/menu_entity.py b/src/fastfood_two/domain/menu/menu_entity.py new file mode 100644 index 0000000..5d0c3e7 --- /dev/null +++ b/src/fastfood_two/domain/menu/menu_entity.py @@ -0,0 +1,34 @@ +from dataclasses import dataclass +from uuid import UUID + +from fastfood_two.domain.core.entity import DomainEntity +from fastfood_two.domain.core.value_obj import DomainValueObject + + +@dataclass(frozen=True) +class MenuId(DomainValueObject): + value: UUID + + +@dataclass(frozen=True) +class Title(DomainValueObject): + value: str + + +@dataclass(frozen=True) +class Description(DomainValueObject): + value: str | None + + +@dataclass +class Menu(DomainEntity[MenuId]): + title: Title + description: Description + + @staticmethod + def create(id: UUID, title: str, description: str | None) -> "Menu": + return Menu( + id=MenuId(id), + title=Title(title), + description=Description(description), + ) diff --git a/src/fastfood_two/infrastructure/menu_gateway.py b/src/fastfood_two/infrastructure/menu_gateway.py index 632c717..649a009 100644 --- a/src/fastfood_two/infrastructure/menu_gateway.py +++ b/src/fastfood_two/infrastructure/menu_gateway.py @@ -1,15 +1,27 @@ +from uuid import UUID + from sqlalchemy import text from sqlalchemy.ext.asyncio import AsyncSession -from fastfood_two.application.contracts.responses import MenuResponse -from fastfood_two.infrastructure.pg_storage.mappers.menu_mapper import entity_to_dto +from fastfood_two.domain.menu.menu_entity import Menu +from fastfood_two.infrastructure.pg_storage.mappers.menu_mapper import ( + db_entity_to_domain, +) class MenuGatewayImpl: def __init__(self, session: AsyncSession) -> None: self._session = session - async def get_all_menus(self) -> list[MenuResponse]: + async def get_all_menus(self) -> list[Menu]: query = text("SELECT * FROM menu;") menus = await self._session.execute(query) - return [entity_to_dto(menu) for menu in menus.scalars().all()] + return [db_entity_to_domain(menu) for menu in menus.scalars().all()] + + async def get_menu_by_id(self, id: UUID) -> Menu | None: ... + + async def insert_menu(self, menu: Menu) -> Menu: ... + + async def update_menu(self, updated_menu: Menu) -> Menu | None: ... + + async def delete_menu(self, id: UUID) -> None: ... diff --git a/src/fastfood_two/infrastructure/pg_storage/mappers/menu_mapper.py b/src/fastfood_two/infrastructure/pg_storage/mappers/menu_mapper.py index 0ea1fd8..32d1cd7 100644 --- a/src/fastfood_two/infrastructure/pg_storage/mappers/menu_mapper.py +++ b/src/fastfood_two/infrastructure/pg_storage/mappers/menu_mapper.py @@ -1,14 +1,14 @@ from typing import TYPE_CHECKING -from fastfood_two.application.contracts.responses import MenuResponse +from fastfood_two.domain.menu.menu_entity import Menu if TYPE_CHECKING: - from fastfood_two.infrastructure.pg_storage.models import Menu + from fastfood_two.infrastructure.pg_storage.models import SQLAMenu -def entity_to_dto(menu: "Menu") -> MenuResponse: - return MenuResponse( +def db_entity_to_domain(menu: "SQLAMenu") -> Menu: + return Menu( id=menu.id, title=menu.title, - description=menu.description or "", + description=menu.description, ) diff --git a/src/fastfood_two/infrastructure/pg_storage/models/__init__.py b/src/fastfood_two/infrastructure/pg_storage/models/__init__.py index 392b292..d1bdc8f 100644 --- a/src/fastfood_two/infrastructure/pg_storage/models/__init__.py +++ b/src/fastfood_two/infrastructure/pg_storage/models/__init__.py @@ -1,10 +1,10 @@ from .base import Base -from .menu_model import Menu +from .menu_model import SQLAMenu from .dish_model import Dish from .common_attrs import uuidpk, str_25 __all__ = [ "Base", - "Menu", + "SQLAMenu", "Dish", ] diff --git a/src/fastfood_two/infrastructure/pg_storage/models/menu_model.py b/src/fastfood_two/infrastructure/pg_storage/models/menu_model.py index 013a47c..63c7f1c 100644 --- a/src/fastfood_two/infrastructure/pg_storage/models/menu_model.py +++ b/src/fastfood_two/infrastructure/pg_storage/models/menu_model.py @@ -5,18 +5,18 @@ from .base import Base from .common_attrs import str_25, uuidpk -class Menu(Base): - __tablename__ = "menu" +class SQLAMenu(Base): + __tablename__ = "menus" id: Mapped[uuidpk] title: Mapped[str_25] description: Mapped[str | None] - parent: Mapped[uuidpk | None] = mapped_column(ForeignKey("menu.id", ondelete="CASCADE"), nullable=True) + parent: Mapped[uuidpk | None] = mapped_column(ForeignKey("menus.id", ondelete="CASCADE"), nullable=True) - submenus: Mapped[list["Menu"]] = relationship( - "Menu", - backref="menu", + submenus: Mapped[list["SQLAMenu"]] = relationship( + "SQLAMenu", + backref="menus", lazy="selectin", cascade="all, delete", ) diff --git a/src/fastfood_two/presentation/fastapi_backend/dependencies.py b/src/fastfood_two/presentation/fastapi_backend/dependencies.py index a339329..2c0f6aa 100644 --- a/src/fastfood_two/presentation/fastapi_backend/dependencies.py +++ b/src/fastfood_two/presentation/fastapi_backend/dependencies.py @@ -5,8 +5,8 @@ from sqlalchemy.ext.asyncio import AsyncEngine, AsyncSession, async_sessionmaker from fastfood_two.application.common.logger import configure_logger from fastfood_two.application.config import Config -from fastfood_two.application.contracts.gateways import MenuGateway from fastfood_two.application.usecases.menu.get_all_menus import GetAllMenus +from fastfood_two.domain.menu.gateway import MenuGateway from fastfood_two.presentation.fastapi_backend.depends.config import get_settings from fastfood_two.presentation.fastapi_backend.depends.gateways import get_menu_gateway from fastfood_two.presentation.fastapi_backend.depends.session import ( diff --git a/src/fastfood_two/presentation/fastapi_backend/depends/gateways.py b/src/fastfood_two/presentation/fastapi_backend/depends/gateways.py index 2620a42..34cfd0a 100644 --- a/src/fastfood_two/presentation/fastapi_backend/depends/gateways.py +++ b/src/fastfood_two/presentation/fastapi_backend/depends/gateways.py @@ -3,7 +3,7 @@ from typing import Annotated from fastapi import Depends from sqlalchemy.ext.asyncio import AsyncSession -from fastfood_two.application.contracts.gateways import MenuGateway +from fastfood_two.domain.menu.gateway import MenuGateway from fastfood_two.infrastructure.menu_gateway import MenuGatewayImpl from fastfood_two.presentation.fastapi_backend.depends.stub import Stub diff --git a/src/fastfood_two/presentation/fastapi_backend/depends/usecases.py b/src/fastfood_two/presentation/fastapi_backend/depends/usecases.py index 2cfacd8..4768271 100644 --- a/src/fastfood_two/presentation/fastapi_backend/depends/usecases.py +++ b/src/fastfood_two/presentation/fastapi_backend/depends/usecases.py @@ -2,10 +2,15 @@ from typing import Annotated from fastapi import Depends -from fastfood_two.application.contracts.gateways import MenuGateway +from fastfood_two.application.usecases.menu.add_menu import AddMenu from fastfood_two.application.usecases.menu.get_all_menus import GetAllMenus +from fastfood_two.domain.menu.gateway import MenuGateway from fastfood_two.presentation.fastapi_backend.depends.stub import Stub def get_all_menus_usecase(gateway: Annotated[MenuGateway, Depends(Stub(MenuGateway))]) -> GetAllMenus: return GetAllMenus(gateway=gateway) + + +def add_menu_usecase(gateway: Annotated[MenuGateway, Depends(Stub(MenuGateway))]) -> AddMenu: + return AddMenu(gateway=gateway) diff --git a/src/fastfood_two/presentation/fastapi_backend/pdmodels/__init__.py b/src/fastfood_two/presentation/fastapi_backend/pdmodels/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/fastfood_two/presentation/fastapi_backend/pdmodels/menu.py b/src/fastfood_two/presentation/fastapi_backend/pdmodels/menu.py new file mode 100644 index 0000000..c2841f2 --- /dev/null +++ b/src/fastfood_two/presentation/fastapi_backend/pdmodels/menu.py @@ -0,0 +1,6 @@ +from pydantic import BaseModel + + +class AddMenuPDModel(BaseModel): + title: str + description: str | None diff --git a/src/fastfood_two/presentation/fastapi_backend/routers/menu.py b/src/fastfood_two/presentation/fastapi_backend/routers/menu.py index 0843c10..6e063f3 100644 --- a/src/fastfood_two/presentation/fastapi_backend/routers/menu.py +++ b/src/fastfood_two/presentation/fastapi_backend/routers/menu.py @@ -2,20 +2,36 @@ from typing import Annotated from fastapi import APIRouter, Depends -from fastfood_two.application.contracts.responses import MenuResponse +from fastfood_two.application.contracts.requests import AddMenuDTO +from fastfood_two.application.contracts.responses import MenuDTO +from fastfood_two.application.usecases.menu.add_menu import AddMenu from fastfood_two.application.usecases.menu.get_all_menus import GetAllMenus from fastfood_two.presentation.fastapi_backend.depends.stub import Stub +from fastfood_two.presentation.fastapi_backend.pdmodels.menu import AddMenuPDModel router = APIRouter(prefix="/menu", tags=["Menu"]) -@router.get("/", response_model=list[MenuResponse]) +@router.get("/", response_model=list[MenuDTO]) async def get_all_menus( usecase: Annotated[GetAllMenus, Depends(Stub(GetAllMenus))], -) -> list[MenuResponse]: +) -> list[MenuDTO]: """Get all menus. Endpoint returns list of all available food menus """ menus = await usecase() return menus + + +@router.post("/", response_model=MenuDTO) +async def add_menu( + request: AddMenuPDModel, + usecase: Annotated[AddMenu, Depends(Stub(AddMenu))], +) -> MenuDTO: + """Get all menus. + + Endpoint returns list of all available food menus + """ + menus = await usecase(request=AddMenuDTO(title=request.title, description=request.description)) + return menus