154 lines
4.4 KiB
Python
154 lines
4.4 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import keyword
|
|
import sys
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
|
|
REPO_ROOT = Path(__file__).resolve().parents[1]
|
|
BACKEND_ROOT = REPO_ROOT / "apps" / "backend"
|
|
OPENAPI_PATH = REPO_ROOT / "packages" / "shared" / "openapi.json"
|
|
TS_TYPES_PATH = REPO_ROOT / "packages" / "shared" / "src" / "api-types.ts"
|
|
|
|
sys.path.insert(0, str(BACKEND_ROOT))
|
|
|
|
from src.presentation.main import app # noqa: E402
|
|
|
|
|
|
GENERATED_HEADER = """// Generated from backend OpenAPI by scripts/generate_openapi_contracts.py.
|
|
// Do not edit by hand.
|
|
|
|
"""
|
|
|
|
|
|
def main() -> None:
|
|
openapi = app.openapi()
|
|
OPENAPI_PATH.write_text(
|
|
json.dumps(openapi, indent=2, sort_keys=True) + "\n",
|
|
encoding="utf-8",
|
|
)
|
|
TS_TYPES_PATH.write_text(render_typescript(openapi), encoding="utf-8")
|
|
print(f"wrote {OPENAPI_PATH}")
|
|
print(f"wrote {TS_TYPES_PATH}")
|
|
|
|
|
|
def render_typescript(openapi: dict[str, Any]) -> str:
|
|
schemas = openapi.get("components", {}).get("schemas", {})
|
|
lines = [GENERATED_HEADER.rstrip(), ""]
|
|
for name in sorted(schemas):
|
|
lines.append(render_schema_type(name, schemas[name], schemas))
|
|
lines.append("")
|
|
return "\n".join(lines).rstrip() + "\n"
|
|
|
|
|
|
def render_schema_type(
|
|
name: str,
|
|
schema: dict[str, Any],
|
|
schemas: dict[str, Any],
|
|
) -> str:
|
|
if schema.get("type") == "object" or "properties" in schema:
|
|
return f"export type {name} = {render_object(schema, schemas, indent=0)};"
|
|
return f"export type {name} = {schema_to_ts(schema, schemas)};"
|
|
|
|
|
|
def render_object(
|
|
schema: dict[str, Any],
|
|
schemas: dict[str, Any],
|
|
*,
|
|
indent: int,
|
|
) -> str:
|
|
properties = schema.get("properties", {})
|
|
required = set(schema.get("required", []))
|
|
pad = " " * indent
|
|
child_pad = " " * (indent + 1)
|
|
|
|
if not properties:
|
|
additional = schema.get("additionalProperties")
|
|
if isinstance(additional, dict):
|
|
return f"Record<string, {schema_to_ts(additional, schemas)}>"
|
|
return "Record<string, unknown>"
|
|
|
|
lines = ["{"]
|
|
for property_name in sorted(properties):
|
|
property_schema = properties[property_name]
|
|
optional = "" if property_name in required else "?"
|
|
lines.append(
|
|
f"{child_pad}{ts_property_name(property_name)}{optional}: "
|
|
f"{schema_to_ts(property_schema, schemas)};"
|
|
)
|
|
lines.append(f"{pad}}}")
|
|
return "\n".join(lines)
|
|
|
|
|
|
def schema_to_ts(schema: dict[str, Any], schemas: dict[str, Any]) -> str:
|
|
if "$ref" in schema:
|
|
return schema["$ref"].removeprefix("#/components/schemas/")
|
|
|
|
if "enum" in schema:
|
|
values = schema["enum"]
|
|
return " | ".join(json.dumps(value) for value in values) or "never"
|
|
|
|
if "const" in schema:
|
|
return json.dumps(schema["const"])
|
|
|
|
if "anyOf" in schema:
|
|
return render_union(schema["anyOf"], schemas)
|
|
|
|
if "oneOf" in schema:
|
|
return render_union(schema["oneOf"], schemas)
|
|
|
|
if "allOf" in schema:
|
|
return " & ".join(schema_to_ts(item, schemas) for item in schema["allOf"])
|
|
|
|
schema_type = schema.get("type")
|
|
if isinstance(schema_type, list):
|
|
return " | ".join(
|
|
schema_type_to_ts(item, schema, schemas) for item in schema_type
|
|
)
|
|
|
|
return schema_type_to_ts(schema_type, schema, schemas)
|
|
|
|
|
|
def render_union(items: list[dict[str, Any]], schemas: dict[str, Any]) -> str:
|
|
rendered = [schema_to_ts(item, schemas) for item in items]
|
|
return " | ".join(rendered) or "unknown"
|
|
|
|
|
|
def schema_type_to_ts(
|
|
schema_type: str | None,
|
|
schema: dict[str, Any],
|
|
schemas: dict[str, Any],
|
|
) -> str:
|
|
if schema_type == "string":
|
|
return "string"
|
|
if schema_type in {"integer", "number"}:
|
|
return "number"
|
|
if schema_type == "boolean":
|
|
return "boolean"
|
|
if schema_type == "null":
|
|
return "null"
|
|
if schema_type == "array":
|
|
return f"{array_item_to_ts(schema.get('items', {}), schemas)}[]"
|
|
if schema_type == "object" or "properties" in schema:
|
|
return render_object(schema, schemas, indent=0)
|
|
return "unknown"
|
|
|
|
|
|
def array_item_to_ts(schema: dict[str, Any], schemas: dict[str, Any]) -> str:
|
|
rendered = schema_to_ts(schema, schemas)
|
|
if " | " in rendered or " & " in rendered:
|
|
return f"({rendered})"
|
|
return rendered
|
|
|
|
|
|
def ts_property_name(name: str) -> str:
|
|
if name.isidentifier() and not keyword.iskeyword(name):
|
|
return name
|
|
return json.dumps(name)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|