Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/example/note/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,13 @@
from .use_case import (
CreateNoteInput,
CreateNoteUseCase,
DeleteNoteInput,
DeleteNoteUseCase,
GetNoteUseCase,
ListNotesInput,
ListNotesUseCase,
UpdateNoteInput,
UpdateNoteUseCase,
)

router = APIRouter(prefix="/notes", tags=["notes"])
Expand All @@ -23,10 +27,17 @@ class CreateNoteBody(BaseModel):
body: str


class UpdateNoteBody(BaseModel):
title: str
body: str


def make_note_router(
list_use_case: ListNotesUseCase,
get_use_case: GetNoteUseCase,
create_use_case: CreateNoteUseCase,
update_use_case: UpdateNoteUseCase,
delete_use_case: DeleteNoteUseCase,
) -> APIRouter:
@router.get("")
async def list_notes(request: Request) -> JSONResponse:
Expand Down Expand Up @@ -59,4 +70,19 @@ async def create_note(body: CreateNoteBody) -> JSONResponse:
{"id": note.id, "title": note.title, "body": note.body}, status_code=201
)

@router.put("/{note_id}")
async def update_note(note_id: int, body: UpdateNoteBody) -> JSONResponse:
errors: list[ValidationError] = []
if not body.title.strip():
errors.append(ValidationError("title", "Title must not be empty.", "required"))
if errors:
raise ValidationException(errors)

note = update_use_case.execute(UpdateNoteInput(note_id, body.title, body.body))
return JSONResponse({"id": note.id, "title": note.title, "body": note.body})

@router.delete("/{note_id}", status_code=204)
async def delete_note(note_id: int) -> None:
delete_use_case.execute(DeleteNoteInput(note_id))

return router
19 changes: 19 additions & 0 deletions src/example/note/repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,12 @@ def find_by_id(self, note_id: int) -> Note | None: ...
@abstractmethod
def save(self, title: str, body: str) -> Note: ...

@abstractmethod
def update(self, note_id: int, title: str, body: str) -> Note | None: ...

@abstractmethod
def delete(self, note_id: int) -> bool: ...

@abstractmethod
def count(self) -> int: ...

Expand All @@ -39,5 +45,18 @@ def save(self, title: str, body: str) -> Note:
self._next_id += 1
return note

def update(self, note_id: int, title: str, body: str) -> Note | None:
if note_id not in self._store:
return None
updated = Note(id=note_id, title=title, body=body)
self._store[note_id] = updated
return updated

def delete(self, note_id: int) -> bool:
if note_id not in self._store:
return False
del self._store[note_id]
return True

def count(self) -> int:
return len(self._store)
33 changes: 33 additions & 0 deletions src/example/note/use_case.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,3 +59,36 @@ def execute(self, note_id: int) -> Note:
if note is None:
raise NoteNotFoundException(note_id)
return note


@dataclass(frozen=True)
class UpdateNoteInput:
note_id: int
title: str
body: str


class UpdateNoteUseCase:
def __init__(self, repository: NoteRepositoryInterface) -> None:
self._repository = repository

def execute(self, input_: UpdateNoteInput) -> Note:
note = self._repository.update(input_.note_id, input_.title, input_.body)
if note is None:
raise NoteNotFoundException(input_.note_id)
return note


@dataclass(frozen=True)
class DeleteNoteInput:
note_id: int


class DeleteNoteUseCase:
def __init__(self, repository: NoteRepositoryInterface) -> None:
self._repository = repository

def execute(self, input_: DeleteNoteInput) -> None:
deleted = self._repository.delete(input_.note_id)
if not deleted:
raise NoteNotFoundException(input_.note_id)
41 changes: 41 additions & 0 deletions tests/example/note/test_list_notes.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,47 @@ def test_get_nonexistent_note_returns_404() -> None:
assert r.status_code == 404


def test_update_note_returns_200() -> None:
client = _client()
r = client.post("/notes", json={"title": "Old", "body": "Old body"})
note_id = r.json()["id"]

r2 = client.put(f"/notes/{note_id}", json={"title": "New", "body": "New body"})
assert r2.status_code == 200
assert r2.json()["title"] == "New"
assert r2.json()["body"] == "New body"


def test_update_nonexistent_note_returns_404() -> None:
r = _client().put("/notes/9999", json={"title": "T", "body": "B"})
assert r.status_code == 404


def test_update_note_empty_title_returns_422() -> None:
client = _client()
r = client.post("/notes", json={"title": "T", "body": "B"})
note_id = r.json()["id"]
r2 = client.put(f"/notes/{note_id}", json={"title": "", "body": "B"})
assert r2.status_code == 422


def test_delete_note_returns_204() -> None:
client = _client()
r = client.post("/notes", json={"title": "T", "body": "B"})
note_id = r.json()["id"]

r2 = client.delete(f"/notes/{note_id}")
assert r2.status_code == 204

r3 = client.get(f"/notes/{note_id}")
assert r3.status_code == 404


def test_delete_nonexistent_note_returns_404() -> None:
r = _client().delete("/notes/9999")
assert r.status_code == 404


def test_health() -> None:
r = _client().get("/health")
assert r.status_code == 200
Expand Down
Loading