Two more oatpp 1.3 spec quirks surfaced when fewo-webapp's #469 increment 2 wired up Orval's strict Zod codegen on top of the v0.4.1 `required: []` strip: - `"type": "Any"` is emitted for `oatpp::Any` / `oatpp::Fields` fields. OpenAPI 3.0 has no "Any" type — the empty schema `{}` (or absence of `type`) is the canonical "any value" form. Strip the offending key recursively across the whole spec. - Some endpoints emit the same path parameter twice (same `name` + same `in: path`). Dedupe by `(name, in)` preserving first occurrence. These were caught only by `@scalar/openapi-parser`'s strict mode (used by Orval's Zod project); the fetch-client project tolerated them. Without both fixes, fewoZod fails with `must NOT have duplicate items` and `must match exactly one schema in oneOf` per affected component. Bumps to v0.4.2. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
102 lines
3.8 KiB
Python
Executable file
102 lines
3.8 KiB
Python
Executable file
#!/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))
|