Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion docs-main/openapi/splice/scan/scan-stream-server.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,26 @@ info:
title: Scan Streaming API
version: 0.0.1
servers:
- url: https://example.com/api/scan
- url: https://scan.sv-1.global.canton.network.c7.digital/api/scan
description: C7
- url: https://scan.sv-1.global.canton.network.cumberland.io/api/scan
description: Cumberland
- url: https://scan.sv-1.global.canton.network.digitalasset.com/api/scan
description: Digital Asset
- url: https://scan.sv-1.global.canton.network.fivenorth.io/api/scan
description: Five North
- url: https://scan.sv-1.global.canton.network.sync.global/api/scan
description: Sync Global
- url: https://scan.sv-1.global.canton.network.lcv.mpch.io/api/scan
description: LCV MPCH
- url: https://scan.sv-1.global.canton.network.mpch.io/api/scan
description: MPCH
- url: https://scan.sv-1.global.canton.network.orb1lp.mpch.io/api/scan
description: Orb1 LP MPCH
- url: https://scan.sv-1.global.canton.network.proofgroup.xyz/api/scan
description: Proof Group
- url: https://scan.sv-1.global.canton.network.tradeweb.com/api/scan
description: Tradeweb
tags:
- name: external
description: |
Expand Down
21 changes: 20 additions & 1 deletion docs-main/openapi/splice/scan/scan.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,26 @@ info:
title: Scan API
version: 0.0.1
servers:
- url: https://example.com/api/scan
- url: https://scan.sv-1.global.canton.network.c7.digital/api/scan
description: C7
- url: https://scan.sv-1.global.canton.network.cumberland.io/api/scan
description: Cumberland
- url: https://scan.sv-1.global.canton.network.digitalasset.com/api/scan
description: Digital Asset
- url: https://scan.sv-1.global.canton.network.fivenorth.io/api/scan
description: Five North
- url: https://scan.sv-1.global.canton.network.sync.global/api/scan
description: Sync Global
- url: https://scan.sv-1.global.canton.network.lcv.mpch.io/api/scan
description: LCV MPCH
- url: https://scan.sv-1.global.canton.network.mpch.io/api/scan
description: MPCH
- url: https://scan.sv-1.global.canton.network.orb1lp.mpch.io/api/scan
description: Orb1 LP MPCH
- url: https://scan.sv-1.global.canton.network.proofgroup.xyz/api/scan
description: Proof Group
- url: https://scan.sv-1.global.canton.network.tradeweb.com/api/scan
description: Tradeweb
tags:
- name: external
description: |
Expand Down
109 changes: 87 additions & 22 deletions scripts/generate_splice_mintlify_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@
DEFAULT_CACHE_DIR = REPO_ROOT / ".internal" / "cache" / "mintlify-openapi" / "splice-openapi"
DEFAULT_DOCS_JSON = REPO_ROOT / "docs-main" / "docs.json"
HTTP_METHODS = {"get", "put", "post", "delete", "options", "head", "patch", "trace"}
SCAN_OPENAPI_PLACEHOLDER_SERVER = "https://example.com/api/scan"
SCAN_OPENAPI_PUBLIC_SERVERS = [
("C7", "https://scan.sv-1.global.canton.network.c7.digital/api/scan"),
("Cumberland", "https://scan.sv-1.global.canton.network.cumberland.io/api/scan"),
("Digital Asset", "https://scan.sv-1.global.canton.network.digitalasset.com/api/scan"),
("Five North", "https://scan.sv-1.global.canton.network.fivenorth.io/api/scan"),
("Sync Global", "https://scan.sv-1.global.canton.network.sync.global/api/scan"),
("LCV MPCH", "https://scan.sv-1.global.canton.network.lcv.mpch.io/api/scan"),
("MPCH", "https://scan.sv-1.global.canton.network.mpch.io/api/scan"),
("Orb1 LP MPCH", "https://scan.sv-1.global.canton.network.orb1lp.mpch.io/api/scan"),
("Proof Group", "https://scan.sv-1.global.canton.network.proofgroup.xyz/api/scan"),
("Tradeweb", "https://scan.sv-1.global.canton.network.tradeweb.com/api/scan"),
]
SCAN_OPENAPI_SERVER_REPLACEMENT_SPECS = {"scan.yaml", "scan-stream-server.yaml"}


def load_json(path: Path) -> dict[str, Any]:
Expand Down Expand Up @@ -241,11 +255,20 @@ def extract_spec_bytes(
return extracted


def render_output_bytes(*, spec_bytes: bytes, output_path: Path) -> bytes:
def render_scan_openapi_servers() -> str:
return "\n".join(
f" - url: {server}\n description: {description}"
for description, server in SCAN_OPENAPI_PUBLIC_SERVERS
)


def render_output_bytes(*, spec_filename: str, spec_bytes: bytes, output_path: Path) -> bytes:
if output_path.suffix not in {".yaml", ".yml"}:
return spec_bytes

text = spec_bytes.decode("utf-8")
if spec_filename in SCAN_OPENAPI_SERVER_REPLACEMENT_SPECS:
text = text.replace(f" - url: {SCAN_OPENAPI_PLACEHOLDER_SERVER}", render_scan_openapi_servers())
filtered_lines = [
line
for line in text.splitlines()
Expand Down Expand Up @@ -351,6 +374,7 @@ def write_managed_specs(
output_path.parent.mkdir(parents=True, exist_ok=True)
output_path.write_bytes(
render_output_bytes(
spec_filename=spec["filename"],
spec_bytes=spec_bytes[spec["filename"]],
output_path=output_path,
)
Expand Down Expand Up @@ -447,20 +471,61 @@ def build_splice_group_pages(*, docs_root: Path, families: list[dict[str, Any]])
return pages


def navigation_pages(payload: dict[str, Any], dropdown_label: str, docs_json_path: Path) -> list[Any]:
navigation = payload.get("navigation")
if not isinstance(navigation, dict):
raise ValueError(f"docs.json missing navigation object: {docs_json_path}")

dropdowns = navigation.get("dropdowns")
if isinstance(dropdowns, list):
dropdown = next(
(item for item in dropdowns if isinstance(item, dict) and item.get("dropdown") == dropdown_label),
None,
)
if dropdown is None:
raise ValueError(f"Dropdown not found in docs.json: {dropdown_label}")
pages = dropdown.get("pages")
if not isinstance(pages, list):
raise ValueError(f"Dropdown does not expose a pages list: {dropdown_label}")
return pages

products = navigation.get("products")
if isinstance(products, list):
product = next(
(item for item in products if isinstance(item, dict) and item.get("product") == dropdown_label),
None,
)
if product is None:
raise ValueError(f"Product not found in docs.json: {dropdown_label}")
pages = product.get("pages")
if not isinstance(pages, list):
raise ValueError(f"Product does not expose a pages list: {dropdown_label}")
return pages

raise ValueError(f"docs.json navigation must define dropdowns or products: {docs_json_path}")


def merge_splice_group_pages(*, existing_pages: list[Any], generated_pages: list[Any]) -> list[Any]:
generated_group_labels = {
item["group"]
for item in generated_pages
if isinstance(item, dict) and isinstance(item.get("group"), str)
}
preserved_pages = [
item
for item in existing_pages
if not (isinstance(item, dict) and item.get("group") in generated_group_labels)
]
return preserved_pages + generated_pages


def update_docs_navigation(
*,
docs_json_path: Path,
source_config: dict[str, Any],
families: list[dict[str, Any]],
) -> None:
payload = load_json(docs_json_path)
navigation = payload.get("navigation")
if not isinstance(navigation, dict):
raise ValueError(f"docs.json missing navigation object: {docs_json_path}")
dropdowns = navigation.get("dropdowns")
if not isinstance(dropdowns, list):
raise ValueError(f"docs.json navigation.dropdowns must be a list: {docs_json_path}")

dropdown_label = source_config.get("nav_dropdown") or "API Reference"
if not isinstance(dropdown_label, str):
raise ValueError("nav_dropdown must be a string")
Expand All @@ -473,19 +538,15 @@ def update_docs_navigation(
enabled_specs = enabled_nav_specs(source_config)
navigation_families = filtered_families_for_navigation(families=families, enabled_specs=enabled_specs)

dropdown = next(
(item for item in dropdowns if isinstance(item, dict) and item.get("dropdown") == dropdown_label),
None,
)
if dropdown is None:
raise ValueError(f"Dropdown not found in docs.json: {dropdown_label}")
pages = dropdown.get("pages")
if not isinstance(pages, list):
raise ValueError(f"Dropdown does not expose a pages list: {dropdown_label}")
pages = navigation_pages(payload, dropdown_label, docs_json_path)

deduped_pages: list[Any] = []
existing_top_group_pages: list[Any] | None = None
for item in pages:
if isinstance(item, dict) and item.get("group") == top_level_group_label:
group_pages = item.get("pages")
if isinstance(group_pages, list):
existing_top_group_pages = group_pages
continue
deduped_pages.append(item)

Expand All @@ -499,14 +560,18 @@ def update_docs_navigation(
insert_at = index + 1
break

generated_pages = build_splice_group_pages(docs_root=docs_json_path.parent, families=navigation_families)
if existing_top_group_pages is not None:
generated_pages = merge_splice_group_pages(
existing_pages=existing_top_group_pages,
generated_pages=generated_pages,
)

deduped_pages.insert(
insert_at,
{
"group": top_level_group_label,
"pages": build_splice_group_pages(docs_root=docs_json_path.parent, families=navigation_families),
},
{"group": top_level_group_label, "pages": generated_pages},
)
dropdown["pages"] = deduped_pages
pages[:] = deduped_pages
docs_json_path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")


Expand Down
110 changes: 110 additions & 0 deletions tests/test_splice_mintlify_openapi.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,41 @@ def write_json(path: Path, payload: object) -> None:
path.write_text(json.dumps(payload, indent=2) + "\n", encoding="utf-8")


def test_splice_openapi_rewrites_scan_server_examples(tmp_path: Path) -> None:
module = load_script_module("generate_splice_mintlify_openapi.py")
spec_bytes = b"""openapi: 3.0.0
servers:
- url: https://example.com/api/scan
paths: {}
"""

rendered_scan = module.render_output_bytes(
spec_filename="scan.yaml",
spec_bytes=spec_bytes,
output_path=tmp_path / "scan.yaml",
).decode("utf-8")
rendered_stream = module.render_output_bytes(
spec_filename="scan-stream-server.yaml",
spec_bytes=spec_bytes,
output_path=tmp_path / "scan-stream-server.yaml",
).decode("utf-8")
rendered_wallet = module.render_output_bytes(
spec_filename="wallet-external.yaml",
spec_bytes=spec_bytes,
output_path=tmp_path / "wallet-external.yaml",
).decode("utf-8")

assert "https://scan.sv-1.global.canton.network.sync.global/api/scan" in rendered_scan
assert "https://scan.sv-1.global.canton.network.digitalasset.com/api/scan" in rendered_scan
assert "https://scan.sv-1.global.canton.network.tradeweb.com/api/scan" in rendered_scan
assert "https://scan.sv-1.global.canton.network.sync.global/api/scan" in rendered_stream
assert rendered_scan.count(" - url: https://scan.sv-1.global.canton.network.") == 10
assert rendered_stream.count(" - url: https://scan.sv-1.global.canton.network.") == 10
assert "https://example.com/api/scan" not in rendered_scan
assert "https://example.com/api/scan" not in rendered_stream
assert "https://example.com/api/scan" in rendered_wallet


def test_splice_openapi_nav_emits_explicit_pages_for_every_spec(tmp_path: Path) -> None:
module = load_script_module("generate_splice_mintlify_openapi.py")
docs_json = tmp_path / "docs-main" / "docs.json"
Expand Down Expand Up @@ -102,3 +137,78 @@ def test_splice_openapi_nav_emits_explicit_pages_for_every_spec(tmp_path: Path)
"POST /registry/metadata/{token-id}",
],
}


def test_splice_openapi_nav_updates_product_navigation_and_preserves_existing_pages(
tmp_path: Path,
) -> None:
module = load_script_module("generate_splice_mintlify_openapi.py")
docs_json = tmp_path / "docs-main" / "docs.json"
write_json(
docs_json,
{
"navigation": {
"products": [
{
"product": "API Reference",
"pages": [
"api-reference",
{"group": "Wallet Gateway", "pages": ["reference/wallet"]},
{
"group": "Splice APIs",
"pages": [
"sdks-tools/api-reference/splice-daml-apis",
{"group": "Scan APIs", "pages": ["stale-scan-entry"]},
],
},
],
}
]
}
},
)
openapi_path = tmp_path / "docs-main" / "openapi" / "splice" / "scan" / "scan.yaml"
openapi_path.parent.mkdir(parents=True, exist_ok=True)
openapi_path.write_text(
"""openapi: 3.0.3
paths:
/v0/scans:
get:
summary: /v0/scans
components: {}
""",
encoding="utf-8",
)
source_config = {
"nav_dropdown": "API Reference",
"top_level_group_label": "Splice APIs",
"insert_after_group": "Wallet Gateway",
"enabled_nav_specs": ["scan.yaml"],
"families": [
{
"group": "Scan APIs",
"specs": [
{
"filename": "scan.yaml",
"nav_label": "Scan API",
"source": "openapi/splice/scan/scan.yaml",
"directory": "reference/splice-scan-api",
}
],
}
],
}

module.update_docs_navigation(
docs_json_path=docs_json,
source_config=source_config,
families=module.normalized_families(source_config),
)

docs = json.loads(docs_json.read_text(encoding="utf-8"))
api_pages = docs["navigation"]["products"][0]["pages"]
splice_group = api_pages[2]
assert splice_group["group"] == "Splice APIs"
assert splice_group["pages"][0] == "sdks-tools/api-reference/splice-daml-apis"
assert splice_group["pages"][1]["group"] == "Scan APIs"
assert splice_group["pages"][1]["pages"][0]["pages"] == ["GET /v0/scans"]