Add Domain entity

main
Сергей Ванюшкин 2024-09-03 22:37:00 +00:00
parent cbcebdffb5
commit fad06c45f8
22 changed files with 199 additions and 38 deletions

View File

@ -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

View File

@ -0,0 +1,7 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class AddMenuDTO:
title: str
description: str | None

View File

@ -3,7 +3,7 @@ from uuid import UUID
@dataclass(frozen=True)
class MenuResponse:
class MenuDTO:
id: UUID
title: str
description: str
description: str | None

View File

@ -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,
)

View File

@ -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
]

View File

@ -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

View File

@ -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

View File

@ -0,0 +1,6 @@
from dataclasses import dataclass
@dataclass(frozen=True)
class DomainValueObject:
pass

View File

View File

@ -0,0 +1,9 @@
from fastfood_two.domain.core.error import DomainError
class MenuNotFoundError(DomainError):
pass
class MenuDataValidationError(DomainError):
pass

View File

@ -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

View File

@ -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),
)

View File

@ -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: ...

View File

@ -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,
)

View File

@ -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",
]

View File

@ -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",
)

View File

@ -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 (

View File

@ -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

View File

@ -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)

View File

@ -0,0 +1,6 @@
from pydantic import BaseModel
class AddMenuPDModel(BaseModel):
title: str
description: str | None

View File

@ -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