diff --git a/src/devhelm/_pagination.py b/src/devhelm/_pagination.py index 7bb0239..be5d2f9 100644 --- a/src/devhelm/_pagination.py +++ b/src/devhelm/_pagination.py @@ -23,6 +23,7 @@ class Page(Generic[T]): has_prev: bool = False total_elements: int | None = None total_pages: int | None = None + next_cursor: str | None = None @dataclass @@ -40,17 +41,18 @@ class _PageEnvelope(BaseModel): Items are validated separately via ``parse_list(model_class, ...)`` so the envelope only describes the surrounding pagination shape; that keeps this layer P5-clean (no casts) without forcing every model to be expressed - twice. ``extra="forbid"`` so unknown envelope keys surface as a typed - ``DevhelmValidationError`` (P1) rather than silently disappearing. + twice. ``extra="ignore"`` so additive response fields (``nextCursor``, + future envelope keys) cannot break ``list()`` — Postel's Law. """ - model_config = ConfigDict(extra="forbid") + model_config = ConfigDict(extra="ignore") data: list[Any] = [] # validated separately hasNext: bool = False hasPrev: bool = False totalElements: int | None = None totalPages: int | None = None + nextCursor: str | None = None class _CursorPageEnvelope(BaseModel): @@ -142,6 +144,7 @@ def fetch_page( has_prev=envelope.hasPrev, total_elements=envelope.totalElements, total_pages=envelope.totalPages, + next_cursor=envelope.nextCursor, ) diff --git a/tests/test_pagination.py b/tests/test_pagination.py new file mode 100644 index 0000000..ed483fe --- /dev/null +++ b/tests/test_pagination.py @@ -0,0 +1,34 @@ +"""Offset-page envelope must tolerate additive TableValueResult fields.""" + +from __future__ import annotations + +from devhelm._pagination import _validate_page + + +def test_page_envelope_accepts_next_cursor() -> None: + envelope = _validate_page( + { + "data": [], + "hasNext": False, + "hasPrev": False, + "totalElements": 0, + "totalPages": 0, + "nextCursor": None, + } + ) + assert envelope.nextCursor is None + assert envelope.data == [] + + +def test_page_envelope_ignores_unknown_keys() -> None: + envelope = _validate_page( + {"data": [], "hasNext": False, "hasPrev": False, "futureField": True} + ) + assert envelope.hasNext is False + + +def test_page_envelope_leaves_items_unparsed() -> None: + envelope = _validate_page( + {"data": [{"id": "not-validated-here"}], "hasNext": False, "hasPrev": False} + ) + assert envelope.data[0]["id"] == "not-validated-here"