""" Normalize an OpenAPI schema dict so backend-specific framing doesn't make two semantically-equivalent schemas compare unequal. Drops: - Top-level OpenAPI envelope fields that vary trivially (info, servers, tags) - Django-only x-mizan-functions sub-fields (form metadata) - Django-only top-level extensions (x-mizan-channels) Sorts: - x-mizan-functions by name - components.schemas keys """ from __future__ import annotations from copy import deepcopy from typing import Any _DJANGO_ONLY_FUNCTION_FIELDS = {"isForm", "formName", "formRole", "formFields", "formFieldsError"} _OPENAPI_ENVELOPE_FIELDS_TO_DROP = {"info", "servers", "tags", "openapi"} def normalize(schema: dict[str, Any]) -> dict[str, Any]: """Return a canonicalized copy suitable for deep-equal comparison.""" out = deepcopy(schema) for k in _OPENAPI_ENVELOPE_FIELDS_TO_DROP: out.pop(k, None) out.pop("x-mizan-channels", None) fns = out.get("x-mizan-functions") or [] for fn in fns: for field in _DJANGO_ONLY_FUNCTION_FIELDS: fn.pop(field, None) out["x-mizan-functions"] = sorted(fns, key=lambda f: f["name"]) components = out.get("components", {}) schemas = components.get("schemas") if isinstance(schemas, dict): components["schemas"] = {k: schemas[k] for k in sorted(schemas)} return out def afi_subset(schema: dict[str, Any]) -> dict[str, Any]: """Just the AFI-essential surface — what every adapter must agree on.""" fns = schema.get("x-mizan-functions") or [] fns_clean = [ {k: v for k, v in fn.items() if k not in _DJANGO_ONLY_FUNCTION_FIELDS} for fn in fns ] return { "x-mizan-functions": sorted(fns_clean, key=lambda f: f["name"]), "x-mizan-contexts": schema.get("x-mizan-contexts", {}), } def function_io_schemas(schema: dict[str, Any]) -> dict[str, Any]: """ Subset of components.schemas containing only the per-function Input/Output models (what the codegen actually consumes for TypeScript type generation). Drops backend-specific noise like HTTPValidationError, ValidationError, that one backend emits and the other doesn't. """ fns = schema.get("x-mizan-functions") or [] expected_names: set[str] = set() for fn in fns: if fn.get("inputType"): expected_names.add(fn["inputType"]) if fn.get("outputType"): expected_names.add(fn["outputType"]) components = schema.get("components", {}) schemas = components.get("schemas", {}) return {name: schemas[name] for name in sorted(expected_names) if name in schemas}