#!/usr/bin/env python3 """Post-process openapi.json before handing it to Orval. oatpp 1.3's Swagger generator produces a valid spec but with rough edges that make the generated client less ergonomic: - Missing operationIds on some endpoints → orval falls back to path-based names that collide. We synthesise an operationId from method + path. - summary/description strings occasionally contain stray control characters. - Some endpoints have no tags, which throws off orval's file-splitting. Run between `fetch-openapi.sh` and `orval` (see package.json `codegen`). """ import json import re import sys from pathlib import Path SRC = Path("openapi.json") if not SRC.exists(): sys.exit(f"{SRC} not found — run fetch-openapi.sh first") spec = json.loads(SRC.read_text()) def op_id(method: str, path: str) -> str: """`getApiAnnouncementsEntityIdHistory` from `GET /api/announcements/{entity_id}/history`.""" parts = [method.lower()] for seg in path.strip("/").split("/"): if seg.startswith("{") and seg.endswith("}"): parts.append(seg[1:-1]) else: parts.append(seg) return re.sub(r"[^A-Za-z0-9]+(\w)", lambda m: m.group(1).upper(), "_".join(parts)) paths = spec.get("paths", {}) for path, methods in paths.items(): for method, op in methods.items(): if not isinstance(op, dict): continue op.setdefault("operationId", op_id(method, path)) op.setdefault("tags", [path.strip("/").split("/")[1] if "/" in path.strip("/") else "default"]) # oatpp 1.3 emits `"required": []` on schemas that have no required fields. # OpenAPI 3.0.x rejects this (`must NOT have fewer than 1 items`), which # breaks strict consumers like Orval's Zod generator. Strip empty arrays — # absence of the keyword has the same semantics. schemas = spec.get("components", {}).get("schemas", {}) stripped_required = 0 for sch in schemas.values(): if isinstance(sch, dict) and sch.get("required") == []: del sch["required"] stripped_required += 1 # oatpp 1.3 emits `"type": "Any"` for `oatpp::Any` / `oatpp::Fields` fields. # `Any` is not a valid OpenAPI 3.0 type — the spec uses an empty schema `{}` # (or `nullable: true` with no type) to mean "any value". Replace `Any` with # an empty schema; recurse so we catch nested cases (object property, # array item, etc.). fixed_any = 0 def fix_any(node): global fixed_any if isinstance(node, dict): if node.get("type") == "Any": del node["type"] fixed_any += 1 for v in node.values(): fix_any(v) elif isinstance(node, list): for item in node: fix_any(item) fix_any(spec) # oatpp 1.3 sometimes emits the same path parameter twice (same name + same # `in: path`). OpenAPI 3.0 rejects duplicate parameters. Dedupe by (name, in) # preserving first occurrence. deduped_params = 0 for path_item in paths.values(): if not isinstance(path_item, dict): continue for op in path_item.values(): if not isinstance(op, dict) or "parameters" not in op: continue seen = set() unique = [] for p in op["parameters"]: key = (p.get("name"), p.get("in")) if key in seen: deduped_params += 1 continue seen.add(key) unique.append(p) op["parameters"] = unique SRC.write_text(json.dumps(spec, indent=2)) report = [f"{len(paths)} paths"] if stripped_required: report.append(f"stripped empty `required` from {stripped_required} schemas") if fixed_any: report.append(f"replaced {fixed_any} invalid `type: Any` with any-schema") if deduped_params: report.append(f"deduped {deduped_params} duplicate parameter(s)") print(f" postprocessed {SRC} — " + ", ".join(report))