Skip to content

Commit 08c9bf8

Browse files
committed
Fix duplicate and misspelled operationIds
- Rewrite operationId generator with proper singularization (sandboxes→sandbox, not sandboxe) - Use 'list' prefix for collection GETs (listSandboxes, listTemplates) - Include version suffix for v2/v3 variants (postTemplatesV3) - Singularize parent resource when followed by path param (GET /sandboxes/{id}/logs → getSandboxLogs) - Dedup check ensures all 52 platform operationIds are unique
1 parent c633fb4 commit 08c9bf8

2 files changed

Lines changed: 93 additions & 52 deletions

File tree

openapi-public.yml

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -728,7 +728,7 @@ paths:
728728
$ref: '#/components/responses/401'
729729
'500':
730730
$ref: '#/components/responses/500'
731-
operationId: getTeams
731+
operationId: listTeams
732732
servers:
733733
- *id006
734734
/teams/{teamID}/metrics:
@@ -825,7 +825,7 @@ paths:
825825
$ref: '#/components/responses/403'
826826
'500':
827827
$ref: '#/components/responses/500'
828-
operationId: getTeamMetricMax
828+
operationId: getTeamMetricsMax
829829
servers:
830830
- *id006
831831
/sandboxes:
@@ -859,7 +859,7 @@ paths:
859859
$ref: '#/components/responses/400'
860860
'500':
861861
$ref: '#/components/responses/500'
862-
operationId: getSandboxes
862+
operationId: listSandboxes
863863
post:
864864
description: Create a sandbox from the template
865865
tags:
@@ -931,7 +931,7 @@ paths:
931931
$ref: '#/components/responses/400'
932932
'500':
933933
$ref: '#/components/responses/500'
934-
operationId: getSandboxes
934+
operationId: listSandboxesV2
935935
servers:
936936
- *id006
937937
/sandboxes/metrics:
@@ -966,7 +966,7 @@ paths:
966966
$ref: '#/components/responses/400'
967967
'500':
968968
$ref: '#/components/responses/500'
969-
operationId: getSandboxeMetrics
969+
operationId: listSandboxesMetrics
970970
servers:
971971
- *id006
972972
/sandboxes/{sandboxID}/logs:
@@ -1007,7 +1007,7 @@ paths:
10071007
$ref: '#/components/responses/401'
10081008
'500':
10091009
$ref: '#/components/responses/500'
1010-
operationId: getSandboxeLogs
1010+
operationId: getSandboxLogs
10111011
servers:
10121012
- *id006
10131013
/sandboxes/{sandboxID}:
@@ -1032,7 +1032,7 @@ paths:
10321032
$ref: '#/components/responses/401'
10331033
'500':
10341034
$ref: '#/components/responses/500'
1035-
operationId: getSandboxes
1035+
operationId: getSandbox
10361036
delete:
10371037
description: Kill a sandbox
10381038
tags:
@@ -1050,7 +1050,7 @@ paths:
10501050
$ref: '#/components/responses/401'
10511051
'500':
10521052
$ref: '#/components/responses/500'
1053-
operationId: deleteSandboxes
1053+
operationId: deleteSandbox
10541054
servers:
10551055
- *id006
10561056
/sandboxes/{sandboxID}/metrics:
@@ -1095,7 +1095,7 @@ paths:
10951095
$ref: '#/components/responses/404'
10961096
'500':
10971097
$ref: '#/components/responses/500'
1098-
operationId: getSandboxeMetrics
1098+
operationId: getSandboxMetrics
10991099
servers:
11001100
- *id006
11011101
/sandboxes/{sandboxID}/pause:
@@ -1118,7 +1118,7 @@ paths:
11181118
$ref: '#/components/responses/401'
11191119
'500':
11201120
$ref: '#/components/responses/500'
1121-
operationId: postSandboxePause
1121+
operationId: postSandboxPause
11221122
servers:
11231123
- *id006
11241124
/sandboxes/{sandboxID}/resume:
@@ -1152,7 +1152,7 @@ paths:
11521152
$ref: '#/components/responses/401'
11531153
'500':
11541154
$ref: '#/components/responses/500'
1155-
operationId: postSandboxeResume
1155+
operationId: postSandboxResume
11561156
servers:
11571157
- *id006
11581158
/sandboxes/{sandboxID}/connect:
@@ -1192,7 +1192,7 @@ paths:
11921192
$ref: '#/components/responses/404'
11931193
'500':
11941194
$ref: '#/components/responses/500'
1195-
operationId: postSandboxeConnect
1195+
operationId: postSandboxConnect
11961196
servers:
11971197
- *id006
11981198
/sandboxes/{sandboxID}/timeout:
@@ -1230,7 +1230,7 @@ paths:
12301230
$ref: '#/components/responses/404'
12311231
'500':
12321232
$ref: '#/components/responses/500'
1233-
operationId: postSandboxeTimeout
1233+
operationId: postSandboxTimeout
12341234
servers:
12351235
- *id006
12361236
/sandboxes/{sandboxID}/refreshes:
@@ -1263,7 +1263,7 @@ paths:
12631263
$ref: '#/components/responses/404'
12641264
'500':
12651265
$ref: '#/components/responses/500'
1266-
operationId: postSandboxeRefreshes
1266+
operationId: postSandboxRefreshes
12671267
servers:
12681268
- *id006
12691269
/sandboxes/{sandboxID}/snapshots:
@@ -1304,7 +1304,7 @@ paths:
13041304
$ref: '#/components/responses/404'
13051305
'500':
13061306
$ref: '#/components/responses/500'
1307-
operationId: postSandboxeSnapshots
1307+
operationId: postSandboxSnapshots
13081308
servers:
13091309
- *id006
13101310
/snapshots:
@@ -1336,7 +1336,7 @@ paths:
13361336
$ref: '#/components/responses/401'
13371337
'500':
13381338
$ref: '#/components/responses/500'
1339-
operationId: getSnapshots
1339+
operationId: listSnapshots
13401340
servers:
13411341
- *id006
13421342
/v3/templates:
@@ -1365,7 +1365,7 @@ paths:
13651365
$ref: '#/components/responses/401'
13661366
'500':
13671367
$ref: '#/components/responses/500'
1368-
operationId: postTemplates
1368+
operationId: postTemplatesV3
13691369
servers:
13701370
- *id006
13711371
/v2/templates:
@@ -1395,7 +1395,7 @@ paths:
13951395
$ref: '#/components/responses/401'
13961396
'500':
13971397
$ref: '#/components/responses/500'
1398-
operationId: postTemplates
1398+
operationId: postTemplatesV2
13991399
servers:
14001400
- *id006
14011401
/templates/{templateID}/files/{hash}:
@@ -1429,7 +1429,7 @@ paths:
14291429
application/json:
14301430
schema:
14311431
$ref: '#/components/schemas/TemplateBuildFileUpload'
1432-
operationId: getTemplateFiles
1432+
operationId: getTemplateFile
14331433
servers:
14341434
- *id006
14351435
/templates:
@@ -1461,7 +1461,7 @@ paths:
14611461
$ref: '#/components/responses/401'
14621462
'500':
14631463
$ref: '#/components/responses/500'
1464-
operationId: getTemplates
1464+
operationId: listTemplates
14651465
post:
14661466
description: Create a new template
14671467
deprecated: true
@@ -1513,7 +1513,7 @@ paths:
15131513
$ref: '#/components/responses/401'
15141514
'500':
15151515
$ref: '#/components/responses/500'
1516-
operationId: getTemplates
1516+
operationId: getTemplate
15171517
post:
15181518
description: Rebuild an template
15191519
deprecated: true
@@ -1540,7 +1540,7 @@ paths:
15401540
$ref: '#/components/responses/401'
15411541
'500':
15421542
$ref: '#/components/responses/500'
1543-
operationId: postTemplates
1543+
operationId: postTemplate
15441544
delete:
15451545
description: Delete a template
15461546
tags:
@@ -1557,7 +1557,7 @@ paths:
15571557
$ref: '#/components/responses/401'
15581558
'500':
15591559
$ref: '#/components/responses/500'
1560-
operationId: deleteTemplates
1560+
operationId: deleteTemplate
15611561
patch:
15621562
description: Update template
15631563
deprecated: true
@@ -1587,7 +1587,7 @@ paths:
15871587
$ref: '#/components/responses/401'
15881588
'500':
15891589
$ref: '#/components/responses/500'
1590-
operationId: patchTemplates
1590+
operationId: patchTemplate
15911591
servers:
15921592
- *id006
15931593
/templates/{templateID}/builds/{buildID}:
@@ -1613,7 +1613,7 @@ paths:
16131613
$ref: '#/components/responses/401'
16141614
'500':
16151615
$ref: '#/components/responses/500'
1616-
operationId: postTemplateBuilds
1616+
operationId: postTemplateBuild
16171617
servers:
16181618
- *id006
16191619
/v2/templates/{templateID}/builds/{buildID}:
@@ -1640,7 +1640,7 @@ paths:
16401640
$ref: '#/components/responses/401'
16411641
'500':
16421642
$ref: '#/components/responses/500'
1643-
operationId: postTemplateBuilds
1643+
operationId: postTemplateBuildV2
16441644
servers:
16451645
- *id006
16461646
/v2/templates/{templateID}:
@@ -1672,7 +1672,7 @@ paths:
16721672
$ref: '#/components/responses/401'
16731673
'500':
16741674
$ref: '#/components/responses/500'
1675-
operationId: patchTemplates
1675+
operationId: patchTemplateV2
16761676
servers:
16771677
- *id006
16781678
/templates/{templateID}/builds/{buildID}/status:
@@ -1808,7 +1808,7 @@ paths:
18081808
$ref: '#/components/responses/404'
18091809
'500':
18101810
$ref: '#/components/responses/500'
1811-
operationId: postTemplateTags
1811+
operationId: postTemplatesTags
18121812
delete:
18131813
description: Delete multiple tags from templates
18141814
tags:
@@ -1832,7 +1832,7 @@ paths:
18321832
$ref: '#/components/responses/404'
18331833
'500':
18341834
$ref: '#/components/responses/500'
1835-
operationId: deleteTemplateTags
1835+
operationId: deleteTemplatesTags
18361836
servers:
18371837
- *id006
18381838
/templates/{templateID}/tags:
@@ -1893,7 +1893,7 @@ paths:
18931893
$ref: '#/components/responses/404'
18941894
'500':
18951895
$ref: '#/components/responses/500'
1896-
operationId: getTemplateAliases
1896+
operationId: getTemplatesAlias
18971897
servers:
18981898
- *id006
18991899
/volumes:
@@ -1917,7 +1917,7 @@ paths:
19171917
$ref: '#/components/responses/401'
19181918
'500':
19191919
$ref: '#/components/responses/500'
1920-
operationId: getVolumes
1920+
operationId: listVolumes
19211921
post:
19221922
description: Create a new team volume
19231923
tags:
@@ -1970,7 +1970,7 @@ paths:
19701970
$ref: '#/components/responses/404'
19711971
'500':
19721972
$ref: '#/components/responses/500'
1973-
operationId: getVolumes
1973+
operationId: getVolume
19741974
delete:
19751975
description: Delete a team volume
19761976
tags:
@@ -1989,7 +1989,7 @@ paths:
19891989
$ref: '#/components/responses/404'
19901990
'500':
19911991
$ref: '#/components/responses/500'
1992-
operationId: deleteVolumes
1992+
operationId: deleteVolume
19931993
servers:
19941994
- *id006
19951995
components:

scripts/generate_openapi_reference.py

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -757,7 +757,23 @@ def fix_spec_issues(spec: dict[str, Any]) -> None:
757757
fixes.append("/templates/{templateID}/files/{hash}: changed 201 → 200 response")
758758

759759
# 10. Generate operationId for platform endpoints that lack one
760+
def _singularize(word: str) -> str:
761+
"""Simple singularization for common API resource names."""
762+
irregulars = {"aliases": "alias", "statuses": "status", "indices": "index"}
763+
if word in irregulars:
764+
return irregulars[word]
765+
if word.endswith("sses"):
766+
return word # "addresses" etc — skip
767+
if word.endswith("ies"):
768+
return word[:-3] + "y"
769+
if word.endswith("ses") or word.endswith("xes") or word.endswith("zes"):
770+
return word[:-2]
771+
if word.endswith("s") and not word.endswith("ss"):
772+
return word[:-1]
773+
return word
774+
760775
op_id_count = 0
776+
seen_ids: dict[str, str] = {} # operationId → path (for dedup)
761777
for ep_path, path_item in paths.items():
762778
# Skip envd endpoints (already have operationIds)
763779
if "/" in ep_path.lstrip("/") and "." in ep_path.split("/")[1]:
@@ -767,28 +783,53 @@ def fix_spec_issues(spec: dict[str, Any]) -> None:
767783
if not op or op.get("operationId"):
768784
continue
769785
# Build operationId from method + path segments
770-
# e.g. GET /templates/{templateID}/builds/{buildID}/status → getTemplateBuildStatus
771-
segments = []
772-
for seg in ep_path.strip("/").split("/"):
773-
if seg.startswith("{") and seg.endswith("}"):
774-
continue # skip path params
775-
# Strip version prefixes
786+
# Include path params to distinguish e.g. /sandboxes vs /sandboxes/{sandboxID}
787+
# e.g. GET /sandboxes/{sandboxID}/logs → getSandboxLogs
788+
# e.g. GET /v2/sandboxes → listSandboxesV2
789+
raw_segments = ep_path.strip("/").split("/")
790+
version_suffix = ""
791+
parts = []
792+
i = 0
793+
while i < len(raw_segments):
794+
seg = raw_segments[i]
776795
if seg in ("v2", "v3"):
796+
version_suffix = seg.upper()
797+
i += 1
777798
continue
778-
segments.append(seg)
779-
# Singularize resource names for sub-resources
780-
# e.g. /sandboxes/{id}/logs → getSandboxLogs
781-
parts = []
782-
for i, seg in enumerate(segments):
783-
if i < len(segments) - 1:
784-
# Sub-resource parent: singularize
785-
s = seg.rstrip("s") if seg.endswith("es") and len(seg) > 3 else (
786-
seg[:-1] if seg.endswith("s") and not seg.endswith("ss") else seg)
787-
parts.append(s)
788-
else:
789-
parts.append(seg)
799+
if seg.startswith("{") and seg.endswith("}"):
800+
# Path param — singularize the previous part if it was a collection
801+
if parts:
802+
parts[-1] = _singularize(parts[-1])
803+
i += 1
804+
continue
805+
parts.append(seg)
806+
i += 1
807+
808+
# For top-level list endpoints (GET /sandboxes, GET /templates),
809+
# use "list" prefix instead of "get" to distinguish from single-resource GETs
810+
prefix = method
811+
if method == "get" and parts and not any(
812+
s.startswith("{") for s in raw_segments[1:]
813+
):
814+
# No path params → it's a list/collection endpoint
815+
# But only if the last segment is plural (a collection name)
816+
last = parts[-1] if parts else ""
817+
if last.endswith("s") and last != "status":
818+
prefix = "list"
819+
790820
name = "".join(p.capitalize() for p in parts)
791-
op["operationId"] = f"{method}{name}"
821+
op_id = f"{prefix}{name}{version_suffix}"
822+
823+
# Dedup: if collision, append a disambiguator
824+
if op_id in seen_ids:
825+
# Try adding "ById" for single-resource variants
826+
if any(s.startswith("{") for s in raw_segments):
827+
op_id = f"{method}{name}ById{version_suffix}"
828+
if op_id in seen_ids:
829+
op_id = f"{method}{name}{version_suffix}_{len(seen_ids)}"
830+
831+
seen_ids[op_id] = ep_path
832+
op["operationId"] = op_id
792833
op_id_count += 1
793834
if op_id_count:
794835
fixes.append(f"Generated operationId for {op_id_count} platform endpoints")

0 commit comments

Comments
 (0)