"""Generate TypeScript types from the OpenAPI schema (no npm needed). Supports the JSON-Schema subset FastAPI/Pydantic emit: objects, arrays, enums/Literals, anyOf with null, $ref, additionalProperties, primitive formats. Output: one `export type X = {...}` per component schema plus `Paths` mapping GET endpoints to their response types. """ from __future__ import annotations from typing import Any HEADER = "// Generated by `politrack openapi --ts` from the FastAPI schema. Do not edit by hand.\n\n" def ts_type(schema: dict[str, Any]) -> str: if "$ref" in schema: return schema["$ref"].rsplit("/", 1)[-1] if "const" in schema: return _literal(schema["const"]) if "enum" in schema: return " | ".join(_literal(v) for v in schema["enum"]) for key in ("anyOf", "oneOf"): if key in schema: parts = [] for sub in schema[key]: part = ts_type(sub) if part not in parts: parts.append(part) return " | ".join(parts) kind = schema.get("type") if kind == "array": inner = ts_type(schema.get("items", {})) return f"({inner})[]" if "|" in inner else f"{inner}[]" if kind == "object": if "properties" in schema: return _object(schema) extra = schema.get("additionalProperties") value = ts_type(extra) if isinstance(extra, dict) and extra else "unknown" return f"Record" if kind in ("integer", "number"): return "number" if kind == "boolean": return "boolean" if kind == "string": return "string" if kind == "null": return "null" return "unknown" def _literal(value: Any) -> str: return f"'{value}'" if isinstance(value, str) else ("null" if value is None else str(value).lower()) def _object(schema: dict[str, Any], indent: str = "") -> str: required = set(schema.get("required", [])) lines = ["{"] for name, prop in schema.get("properties", {}).items(): optional = "" if name in required else "?" lines.append(f"{indent} {name}{optional}: {ts_type(prop)};") lines.append(f"{indent}}}") return "\n".join(lines) def to_typescript(openapi: dict[str, Any]) -> str: out = [HEADER] for name, schema in sorted(openapi.get("components", {}).get("schemas", {}).items()): if name in ("HTTPValidationError", "ValidationError"): continue out.append(f"export type {name} = {ts_type(schema)};\n") out.append("\nexport type Paths = {") for path, ops in sorted(openapi.get("paths", {}).items()): get = ops.get("get") if not get: continue content = get.get("responses", {}).get("200", {}).get("content", {}).get("application/json", {}) out.append(f" '{path}': {ts_type(content.get('schema', {})) if content.get('schema') else 'unknown'};") out.append("};\n") return "\n".join(out)