Source code for loom.rest.model

"""Declarative REST interface model.

Defines the data structures that describe how UseCases are exposed over HTTP.
No FastAPI or transport-specific code lives here — this module is the
declaration layer; :mod:`loom.rest.compiler` validates and compiles it at
startup and :mod:`loom.rest.router_runtime` binds it to FastAPI.

Usage::

    class UserRestInterface(RestInterface[User]):
        prefix = "/users"
        tags = ("Users",)
        auto = True
        include = ("create", "get", "list", "update")
        routes = (
            RestRoute(use_case=CreateUserUseCase, method="POST", path="/"),
            RestRoute(use_case=GetUserUseCase, method="GET", path="/{user_id}"),
            RestRoute(use_case=ListUsersUseCase, method="GET", path="/"),
            RestRoute(
                use_case=UpdateUserUseCase,
                method="PATCH",
                path="/{user_id}",
                summary="Partial update user",
            ),
        )
"""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import Any, Generic, TypeVar, get_args, get_origin

from loom.core.repository.abc.query import PaginationMode
from loom.core.use_case.use_case import UseCase

T = TypeVar("T")

__all__ = [
    "PaginationMode",
    "RestApiDefaults",
    "RestInterface",
    "RestRoute",
]


[docs] @dataclass(frozen=True) class RestRoute: """Declaration of a single HTTP endpoint bound to a UseCase. All fields except ``use_case``, ``method``, and ``path`` are optional and override interface-level or global defaults when set. Args: use_case: Concrete UseCase class that handles this endpoint. method: HTTP method in uppercase (``"GET"``, ``"POST"``, etc.). path: Path relative to the interface prefix. May include path parameters (e.g. ``"/{user_id}"``). summary: Short OpenAPI summary for the endpoint. description: Longer OpenAPI description. status_code: Default HTTP success status code. Defaults to ``200``. pagination_mode: Override pagination strategy for this route. When ``None``, inherits from the interface or global default. profile_default: Default query profile for this route. When empty, inherits from the interface or global default. allowed_profiles: Profiles that callers may request. When empty, inherits from the interface or global default. expose_profile: Whether to expose ``?profile=`` as a public query parameter for this route. Defaults to ``False``. allow_pagination_override: Whether callers may override the resolved pagination mode using query parameters. When ``None``, inherits from the interface or global default. Example:: RestRoute( use_case=UpdateUserUseCase, method="PATCH", path="/{user_id}", summary="Partial update user", status_code=200, ) """ use_case: type[UseCase[Any, Any]] method: str path: str summary: str = "" description: str = "" status_code: int = 200 pagination_mode: PaginationMode | None = None allow_pagination_override: bool | None = None profile_default: str = "" allowed_profiles: tuple[str, ...] = () expose_profile: bool = False
def _extract_model_type(cls: type[Any]) -> type[Any] | None: """Return the concrete model type from ``RestInterface[Model]``. Walks ``__orig_bases__`` to find the parameterised ``RestInterface`` and extracts its first type argument. Returns ``None`` when the class does not carry a concrete type parameter (e.g. intermediate abstract bases). Args: cls: ``RestInterface`` subclass to inspect. Returns: Concrete model type, or ``None`` if not resolvable. """ for base in getattr(cls, "__orig_bases__", ()): if get_origin(base) is not RestInterface: continue args = get_args(base) if args and isinstance(args[0], type): return args[0] return None
[docs] class RestInterface(Generic[T]): """Base class for declarative REST interface definitions. Subclass to expose one or more UseCases under a common HTTP prefix. All class attributes have sensible defaults; override only what differs. Attributes: prefix: URL prefix for all routes in this interface (e.g. ``"/users"``). tags: OpenAPI tags applied to every route. auto: When ``True``, signals intent to use standard CRUD URL conventions. include: Whitelist of CRUD operation names to expose when ``auto=True``. Accepted values: ``"create"``, ``"get"``, ``"list"``, ``"update"``, ``"delete"``. Empty tuple means all operations are allowed. routes: Explicit route declarations. Custom routes always override auto-CRUD routes with the same ``(method, path)``. pagination_mode: Default pagination strategy for list endpoints in this interface. Overridden per-route by :attr:`RestRoute.pagination_mode`. profile_default: Default query profile for routes in this interface. allowed_profiles: Profiles available to callers of routes in this interface. expose_profile: Whether this interface publicly accepts ``?profile=...`` by default. Can be overridden per-route. allow_pagination_override: Whether list endpoints may switch pagination mode from query parameters by default. Example:: class OrderRestInterface(RestInterface[Order]): prefix = "/orders" tags = ("Orders",) auto = True include = ("create", "get", "list") routes = ( RestRoute(use_case=CreateOrderUseCase, method="POST", path="/"), RestRoute(use_case=GetOrderUseCase, method="GET", path="/{order_id}"), RestRoute(use_case=ListOrdersUseCase, method="GET", path="/"), ) """ prefix: str = "" tags: tuple[str, ...] = () auto: bool = False include: tuple[str, ...] = () routes: tuple[RestRoute, ...] = () pagination_mode: PaginationMode | None = None allow_pagination_override: bool | None = None profile_default: str = "" allowed_profiles: tuple[str, ...] = () expose_profile: bool = False def __init_subclass__(cls, **kwargs: Any) -> None: """Auto-populate routes when ``auto=True`` and no routes are declared. Fires whenever a subclass of ``RestInterface`` is defined. Calls :func:`~loom.rest.autocrud.build_auto_routes` only when: - ``cls.auto`` is ``True`` - ``cls.routes`` is empty (user-declared routes take precedence) - The class carries a concrete model type parameter """ super().__init_subclass__(**kwargs) if not cls.auto or cls.routes: return model_type = _extract_model_type(cls) if model_type is None: return from loom.rest.autocrud import build_auto_routes # lazy — avoids circular import cls.routes = build_auto_routes(model_type, cls.include)
[docs] @dataclass(frozen=True) class RestApiDefaults: """Global REST API defaults applied when interface or route level is unset. Args: pagination_mode: Default pagination strategy. Defaults to :attr:`PaginationMode.OFFSET`. profile_default: Default query profile name. Defaults to ``"default"``. allowed_profiles: Globally allowed profiles. Defaults to empty (no restriction imposed at global level). allow_pagination_override: Whether query parameters may override the configured pagination mode. Defaults to ``True`` for backwards compatibility. Example:: defaults = RestApiDefaults( pagination_mode=PaginationMode.CURSOR, profile_default="summary", ) """ pagination_mode: PaginationMode = PaginationMode.OFFSET allow_pagination_override: bool = True profile_default: str = "default" allowed_profiles: tuple[str, ...] = field(default_factory=tuple)