Ctrl K
ComunidadGuías…

scripts/generate_overlay.py

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
#!/usr/bin/env python3
"""
generate_overlay.py — Generates .speakeasy/overlays/flexprice-sdk.yaml

Reads docs/swagger/swagger-3-0.json and produces a Speakeasy overlay that:
  - Strips dto. prefix from all 218 dto.* schemas
  - Renames top-level entity response types (e.g. CustomerResponse → Customer)
  - Renames errors.* schemas to clean names
  - Renames the 'error' property on ErrorResponse → 'detail' (avoids Error_ in Go)
  - Patches 177 timestamp fields with format: date-time
"""

import json
import re
import sys
from pathlib import Path

SPEC_PATH = Path("docs/swagger/swagger-3-0.json")
OVERLAY_PATH = Path(".speakeasy/overlays/flexprice-sdk.yaml")

# Top-level entity response types — strip Response suffix for cleaner SDK names
ENTITY_RENAMES = {
    "dto.CustomerResponse":     "Customer",
    "dto.SubscriptionResponse": "Subscription",
    "dto.InvoiceResponse":      "Invoice",
    "dto.PlanResponse":         "Plan",
    "dto.PriceResponse":        "Price",
    "dto.AddonResponse":        "Addon",
    "dto.WalletResponse":       "Wallet",
    "dto.PaymentResponse":      "Payment",
    "dto.CouponResponse":       "Coupon",
    "dto.FeatureResponse":      "Feature",
}

# Timestamp-like field name patterns — any string field matching these gets format: date-time
# Note: do not use _period — e.g. billing_period is an enum (MONTHLY), not a timestamp.
TIMESTAMP_PATTERNS = re.compile(
    r"(_at|_date|_start|_end|_time|_anchor|expires_at|expiry|"
    r"due_date|close_time|archived_at|applied_at|executed_at|failed_at|"
    r"finalized_at|completed_at|last_used_at|balance_updated_at|_due_lte)$"
)


def quote(s: str) -> str:
    """Wrap a schema name in double-quoted JSONPath bracket notation (RFC 9535)."""
    return f'["{s}"]'


def build_actions(spec: dict) -> list:
    actions = []
    schemas = spec.get("components", {}).get("schemas", {})

    # ── 1. Strip dto. prefix & apply entity renames ─────────────────────────
    dto_schemas = [n for n in schemas if n.startswith("dto.")]
    if dto_schemas:
        for name in dto_schemas:
            if name in ENTITY_RENAMES:
                override = ENTITY_RENAMES[name]
            else:
                override = name[4:]  # strip "dto."
            actions.append({
                "target": f"$.components.schemas{quote(name)}",
                "update": {"x-speakeasy-name-override": override},
            })
    else:
        # Schema names are already clean — add entity renames directly
        for clean_name, override in ENTITY_RENAMES.items():
            schema_name = clean_name[4:]  # strip "dto." to get "CustomerResponse"
            if schema_name in schemas:
                actions.append({
                    "target": f"$.components.schemas{quote(schema_name)}",
                    "update": {"x-speakeasy-name-override": override},
                })

    # ── 2. Rename errors.* schemas ──────────────────────────────────────────
    for name in schemas:
        if not name.startswith("errors."):
            continue
        override = name[7:]  # strip "errors."
        actions.append({
            "target": f"$.components.schemas{quote(name)}",
            "update": {"x-speakeasy-name-override": override},
        })

    # ── 3. Rename 'error' property on ErrorResponse → 'detail' ─────────────
    #    The JSON field name 'error' is a Go reserved keyword; Speakeasy
    #    escapes it to Error_ which is confusing. Renaming to 'detail' in
    #    the SDK (via x-speakeasy-name-override on the property) fixes this.
    if "errors.ErrorResponse" in schemas:
        err_props = schemas["errors.ErrorResponse"].get("properties", {})
        if "error" in err_props:
            actions.append({
                "target": (
                    f"$.components.schemas{quote('errors.ErrorResponse')}"
                    f".properties{quote('error')}"
                ),
                "update": {"x-speakeasy-name-override": "detail"},
            })

    # ── 4. Patch timestamp fields with format: date-time ────────────────────
    def _get_properties(schema: dict) -> list[tuple[str, dict, str]]:
        """List (prop_name, prop_schema, path_suffix) for each property location.

        path_suffix is the JSONPath fragment after the schema object up to its
        ``properties`` map, e.g. ``.properties`` or ``.allOf[0].properties``, so the
        overlay target is ``...schemas["Name"]{path_suffix}["propName"]``.
        """
        out: list[tuple[str, dict, str]] = []

        def walk(node: dict, path_prefix: str) -> None:
            props = node.get("properties")
            if isinstance(props, dict):
                suffix = f"{path_prefix}.properties" if path_prefix else ".properties"
                for prop_name, prop in props.items():
                    if isinstance(prop, dict):
                        out.append((prop_name, prop, suffix))
            for combiner in ("allOf", "anyOf", "oneOf"):
                subs = node.get(combiner)
                if not isinstance(subs, list):
                    continue
                for i, sub in enumerate(subs):
                    if isinstance(sub, dict):
                        child = (
                            f"{path_prefix}.{combiner}[{i}]"
                            if path_prefix
                            else f".{combiner}[{i}]"
                        )
                        walk(sub, child)

        if isinstance(schema, dict):
            walk(schema, "")
        return out

    for schema_name, schema in schemas.items():
        for prop_name, prop, path_suffix in _get_properties(schema):
            if (
                prop.get("type") == "string"
                and prop.get("format") != "date-time"
                and TIMESTAMP_PATTERNS.search(prop_name)
            ):
                actions.append({
                    "target": (
                        f"$.components.schemas{quote(schema_name)}"
                        f"{path_suffix}{quote(prop_name)}"
                    ),
                    "update": {"format": "date-time"},
                })

    return actions


def write_overlay(actions: list) -> None:
    # Write as YAML manually to avoid requiring PyYAML and to control formatting
    lines = [
        "overlay: 1.0.0",
        "info:",
        "  title: Flexprice SDK and MCP customizations",
        "  version: 2.1.0",
        "actions:",
    ]
    for action in actions:
        target = action["target"]
        update = action["update"]
        lines.append(f"  - target: '{target}'")
        lines.append("    update:")
        for k, v in update.items():
            lines.append(f"      {k}: {json.dumps(v)}")
    OVERLAY_PATH.write_text("\n".join(lines) + "\n", encoding="utf-8")


def main() -> None:
    if not SPEC_PATH.exists():
        print(f"ERROR: {SPEC_PATH} not found. Run from repo root.", file=sys.stderr)
        sys.exit(1)

    spec = json.loads(SPEC_PATH.read_text(encoding="utf-8"))
    actions = build_actions(spec)
    write_overlay(actions)

    print(f"Written {len(actions)} overlay actions to {OVERLAY_PATH}")

    # Print a summary breakdown
    name_overrides = sum(1 for a in actions if "x-speakeasy-name-override" in a["update"])
    ts_patches = sum(1 for a in actions if a["update"].get("format") == "date-time")
    print(f"  Name overrides:   {name_overrides}")
    print(f"  Timestamp patches: {ts_patches}")


if __name__ == "__main__":
    main()