"""Envelope + camelCase renderer (``API-STANDARD.md`` §3, §4).""" from __future__ import annotations from typing import Any from django.utils import timezone from djangorestframework_camel_case.render import CamelCaseJSONRenderer __all__ = ["EnvelopeJSONRenderer"] _PAGINATED_KEYS = {"results", "next", "previous"} class EnvelopeJSONRenderer(CamelCaseJSONRenderer): """Wraps every response in the standard envelope and camelCases its keys. * success (status < 400): ``{success: true, data, error: null, meta}`` * error (status >= 400): ``{success: false, data: null, error, meta}`` ``meta`` always carries ``requestId`` and ``timestamp``; it carries ``tenantId`` when a tenant is resolved and ``pagination`` for list responses produced by :class:`infrasynth.api.pagination.CursorPagination`. """ def render( self, data: Any, accepted_media_type: str | None = None, renderer_context: dict[str, Any] | None = None, ) -> bytes: renderer_context = renderer_context or {} response = renderer_context.get("response") request = renderer_context.get("request") status_code = getattr(response, "status_code", 200) or 200 meta: dict[str, Any] = { "requestId": getattr(request, "request_id", "") if request else "", "timestamp": timezone.now().isoformat().replace("+00:00", "Z"), } tenant = getattr(request, "tenant", None) if request else None if tenant is not None: meta["tenantId"] = str(getattr(tenant, "pk", tenant)) if status_code >= 400: body: dict[str, Any] = {"success": False, "data": None, "error": data, "meta": meta} else: payload = data if isinstance(payload, dict) and _PAGINATED_KEYS.issubset(payload.keys()): meta["pagination"] = { "nextCursor": payload.get("next"), "prevCursor": payload.get("previous"), "pageSize": payload.get("page_size"), } payload = payload.get("results") body = {"success": True, "data": payload, "error": None, "meta": meta} return super().render(body, accepted_media_type, renderer_context)