"""# Summary Shared interface helpers — service resolution, serialization, pagination and foreign-key expansion used by every route module. --- FK fields on the wire may arrive either as a bare integer id (from an ``x-fk`` picker) or as an already-expanded object. The wire contract always *returns* fully expanded objects, so create/update handlers normalize the input through :func:`expand_ref` before persisting. """ from fastapi import HTTPException, Request def svc(request: Request, name: str): """Resolve a CRUD service from app state by resource name. Args: request (Request): Incoming request whose app.state carries resources. name (str): Resource key suffix, e.g. ``"parent"`` → ``parent_svc``. Returns: Any: The named CRUD service. """ return request.app.state.db_resources[f"{name}_svc"] def dump(doc) -> dict: """Serialize a document to a JSON-ready mapping with an integer id. Args: doc (Any): Pydantic document to serialize. Returns: dict: JSON-mode mapping (``by_alias`` so ``_id`` maps back to ``id``). """ return doc.model_dump(mode="json", by_alias=True) def paginate(items: list, limit: int, offset: int) -> dict: """Slice ``items`` into the ``{total, items}`` pagination envelope. A ``limit`` of 0 means "all remaining items" (used by react-openapi for FK option lists and counts). Args: items (list): Full result list. limit (int): Page size; ``0`` returns everything after ``offset``. offset (int): Number of items to skip. Returns: dict: ``{"total": int, "items": list}``. """ offset = max(offset, 0) page = items[offset:] if limit == 0 else items[offset : offset + limit] return {"total": len(items), "items": page} async def expand_ref(svc_obj, value, label: str) -> dict | None: """Normalize a foreign-key reference into a fully expanded object. Args: svc_obj (Any): Target resource CRUD service used for lookups. value (dict | int | None): Bare id, ``{"id": N}`` stub, or full object. label (str): Human label used in 422 error messages. Returns: dict | None: Expanded object, or ``None`` when ``value`` is ``None``. Raises: HTTPException: 422 when the reference cannot be resolved to a document. """ if value is None: return None if isinstance(value, dict): if set(value.keys()) == {"id"}: value = value["id"] else: return value if isinstance(value, int): doc = await svc_obj.read(value) if doc is None: raise HTTPException(status_code=422, detail=f"{label} {value} not found") return dump(doc) raise HTTPException(status_code=422, detail=f"invalid {label} reference") async def expand_ref_list(svc_obj, value, label: str) -> list[dict]: """Normalize a list foreign-key reference into expanded objects. Args: svc_obj (Any): Target resource CRUD service used for lookups. value (list | None): List of bare ids, stubs, or full objects. label (str): Human label used in 422 error messages. Returns: list[dict]: Expanded objects. Raises: HTTPException: 422 when any reference cannot be resolved. """ if value is None: return [] if not isinstance(value, list): raise HTTPException(status_code=422, detail=f"invalid {label} list") results = [] for entry in value: results.append(await expand_ref(svc_obj, entry, label)) return results async def read_or_404(svc_obj, id: int, label: str): """Read a document by id or raise a 404 HTTPException. Args: svc_obj (Any): CRUD service to read from. id (int): Document identifier. label (str): Human label used in the 404 detail message. Returns: Any: The found document. Raises: HTTPException: 404 when no document matches ``id``. """ doc = await svc_obj.read(id) if doc is None: raise HTTPException(status_code=404, detail=f"{label} not found") return doc async def delete_or_404(svc_obj, id: int, label: str) -> None: """Delete a document by id or raise a 404 HTTPException. Args: svc_obj (Any): CRUD service to delete from. id (int): Document identifier. label (str): Human label used in the 404 detail message. Raises: HTTPException: 404 when no document matches ``id``. """ ok = await svc_obj.delete(id) if not ok: raise HTTPException(status_code=404, detail=f"{label} not found")