From dda0fca1a6c5e767fcb2758e2f763ed048c7b9ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Huvar?= Date: Tue, 23 Jun 2026 17:20:29 +0200 Subject: [PATCH 1/3] docs: normalize API reference version summaries --- scripts/generate_openapi_reference.py | 35 +++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/scripts/generate_openapi_reference.py b/scripts/generate_openapi_reference.py index d6b6a9d7..f2fcdc36 100755 --- a/scripts/generate_openapi_reference.py +++ b/scripts/generate_openapi_reference.py @@ -673,6 +673,35 @@ def add_operation_ids(spec: dict[str, Any]) -> None: print(f"==> Added {count} operationIds to envd endpoints") +VERSIONED_PLATFORM_PATH_RE = re.compile(r"^/(v\d+)(?:/|$)", re.IGNORECASE) + + +def normalize_versioned_summaries(spec: dict[str, Any]) -> int: + """Append `(vN)` to versioned platform endpoint summaries when missing.""" + count = 0 + for ep_path, path_item in spec.get("paths", {}).items(): + match = VERSIONED_PLATFORM_PATH_RE.match(ep_path) + if not match: + continue + + version_label = match.group(1).lower() + for method in ("get", "post", "put", "patch", "delete", "head", "options"): + op = path_item.get(method) + if not op: + continue + + summary = op.get("summary") + if not isinstance(summary, str) or not summary.strip(): + continue + if re.search(rf"\({re.escape(version_label)}\)", summary, re.IGNORECASE): + continue + + op["summary"] = f"{summary.rstrip()} ({version_label})" + count += 1 + + return count + + STREAMING_ENDPOINTS = { "/filesystem.Filesystem/WatchDir", "/process.Process/Start", @@ -1152,6 +1181,12 @@ def _singularize(word: str) -> str: if summary_count: fixes.append(f"Added summary to {summary_count} platform endpoints") + normalized_summary_count = normalize_versioned_summaries(spec) + if normalized_summary_count: + fixes.append( + f"Normalized version suffix on {normalized_summary_count} versioned endpoint summaries" + ) + # 27. Replace nullable: true with OpenAPI 3.1.0 type arrays # In 3.1.0, nullable was removed. Use type: ["string", "null"] instead, # or oneOf with type: 'null' for $ref properties. From d8548003135d98d30678e23a7f7b7f2ec699f199 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Huvar?= Date: Tue, 23 Jun 2026 17:28:57 +0200 Subject: [PATCH 2/3] docs: regenerate API reference version summaries --- scripts/generate_openapi_reference.py | 32 +++++++++++++++++++++++---- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/scripts/generate_openapi_reference.py b/scripts/generate_openapi_reference.py index f2fcdc36..63552956 100755 --- a/scripts/generate_openapi_reference.py +++ b/scripts/generate_openapi_reference.py @@ -673,18 +673,34 @@ def add_operation_ids(spec: dict[str, Any]) -> None: print(f"==> Added {count} operationIds to envd endpoints") -VERSIONED_PLATFORM_PATH_RE = re.compile(r"^/(v\d+)(?:/|$)", re.IGNORECASE) +VERSIONED_PLATFORM_SEGMENT_RE = re.compile(r"^(v\d+)([A-Za-z][^/]*)?$", re.IGNORECASE) + + +def extract_leading_version_label(ep_path: str) -> str | None: + """Return `vN` for a versioned leading path segment like `v2` or `v2templates`.""" + first_segment = ep_path.lstrip("/").split("/", 1)[0] + match = VERSIONED_PLATFORM_SEGMENT_RE.match(first_segment) + if not match: + return None + return match.group(1).lower() + + +def split_leading_version_segment(segment: str) -> tuple[str | None, str | None]: + """Split `vNfoo` into (`VN`, `foo`) and `vN` into (`VN`, None).""" + match = VERSIONED_PLATFORM_SEGMENT_RE.match(segment) + if not match: + return None, None + return match.group(1).upper(), match.group(2) def normalize_versioned_summaries(spec: dict[str, Any]) -> int: """Append `(vN)` to versioned platform endpoint summaries when missing.""" count = 0 for ep_path, path_item in spec.get("paths", {}).items(): - match = VERSIONED_PLATFORM_PATH_RE.match(ep_path) - if not match: + version_label = extract_leading_version_label(ep_path) + if not version_label: continue - version_label = match.group(1).lower() for method in ("get", "post", "put", "patch", "delete", "head", "options"): op = path_item.get(method) if not op: @@ -907,6 +923,14 @@ def _singularize(word: str) -> str: i = 0 while i < len(raw_segments): seg = raw_segments[i] + if i == 0: + detected_version, remainder = split_leading_version_segment(seg) + if detected_version: + version_suffix = detected_version + if remainder: + parts.append(remainder) + i += 1 + continue if seg in ("v2", "v3"): version_suffix = seg.upper() i += 1 From d9a6c9bf63c07baf38dff6e806b563ed697ce9fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Luk=C3=A1=C5=A1=20Huvar?= Date: Wed, 24 Jun 2026 13:37:28 +0200 Subject: [PATCH 3/3] docs: synthesize versioned API summaries --- scripts/generate_openapi_reference.py | 81 +++++++++++++++++++++------ 1 file changed, 63 insertions(+), 18 deletions(-) diff --git a/scripts/generate_openapi_reference.py b/scripts/generate_openapi_reference.py index 63552956..ccb96a4c 100755 --- a/scripts/generate_openapi_reference.py +++ b/scripts/generate_openapi_reference.py @@ -693,8 +693,64 @@ def split_leading_version_segment(segment: str) -> tuple[str | None, str | None] return match.group(1).upper(), match.group(2) +def singularize(word: str) -> str: + """Simple singularization for common API resource names.""" + irregulars = {"aliases": "alias", "statuses": "status", "indices": "index"} + if word in irregulars: + return irregulars[word] + if word.endswith("sses"): + return word # "addresses" etc - skip + if word.endswith("ies"): + return word[:-3] + "y" + if word.endswith("ses") or word.endswith("xes") or word.endswith("zes"): + return word[:-2] + if word.endswith("s") and not word.endswith("ss"): + return word[:-1] + return word + + +def titleize_resource_name(name: str) -> str: + return name.replace("-", " ") + + +def synthesize_versioned_summary(ep_path: str, method: str) -> str | None: + """Build a readable fallback summary for versioned platform endpoints.""" + raw_segments = ep_path.strip("/").split("/") + parts: list[str] = [] + + for i, seg in enumerate(raw_segments): + if i == 0: + _, remainder = split_leading_version_segment(seg) + if extract_leading_version_label(ep_path): + if remainder: + parts.append(remainder) + continue + if seg.startswith("{") and seg.endswith("}"): + if parts: + parts[-1] = singularize(parts[-1]) + continue + parts.append(seg) + + if not parts: + return None + + resource = titleize_resource_name(parts[-1]) + if method == "get": + if not any(seg.startswith("{") for seg in raw_segments[1:]) and parts[-1].endswith("s"): + return f"List {resource}" + return f"Get {resource}" + if method == "post": + return f"Create {titleize_resource_name(singularize(parts[-1]))}" + if method in {"put", "patch"}: + return f"Update {titleize_resource_name(singularize(parts[-1]))}" + if method == "delete": + return f"Delete {titleize_resource_name(singularize(parts[-1]))}" + + return None + + def normalize_versioned_summaries(spec: dict[str, Any]) -> int: - """Append `(vN)` to versioned platform endpoint summaries when missing.""" + """Ensure versioned platform summaries exist and end with `(vN)`.""" count = 0 for ep_path, path_item in spec.get("paths", {}).items(): version_label = extract_leading_version_label(ep_path) @@ -708,7 +764,11 @@ def normalize_versioned_summaries(spec: dict[str, Any]) -> int: summary = op.get("summary") if not isinstance(summary, str) or not summary.strip(): - continue + summary = synthesize_versioned_summary(ep_path, method) + if not summary: + continue + op["summary"] = summary + count += 1 if re.search(rf"\({re.escape(version_label)}\)", summary, re.IGNORECASE): continue @@ -888,21 +948,6 @@ def fix_spec_issues(spec: dict[str, Any]) -> None: fixes.append("/templates/{templateID}/files/{hash}: changed 201 → 200 response") # 10. Generate operationId for platform endpoints that lack one - def _singularize(word: str) -> str: - """Simple singularization for common API resource names.""" - irregulars = {"aliases": "alias", "statuses": "status", "indices": "index"} - if word in irregulars: - return irregulars[word] - if word.endswith("sses"): - return word # "addresses" etc - skip - if word.endswith("ies"): - return word[:-3] + "y" - if word.endswith("ses") or word.endswith("xes") or word.endswith("zes"): - return word[:-2] - if word.endswith("s") and not word.endswith("ss"): - return word[:-1] - return word - op_id_count = 0 seen_ids: dict[str, str] = {} # operationId → path (for dedup) for ep_path, path_item in paths.items(): @@ -938,7 +983,7 @@ def _singularize(word: str) -> str: if seg.startswith("{") and seg.endswith("}"): # Path param - singularize the previous part if it was a collection if parts: - parts[-1] = _singularize(parts[-1]) + parts[-1] = singularize(parts[-1]) i += 1 continue parts.append(seg)