feat: add --api OpenAPI build mode with docs/api scheme and lib-prefixed nav
This commit is contained in:
114
docforge/cli/api_utils.py
Normal file
114
docforge/cli/api_utils.py
Normal file
@@ -0,0 +1,114 @@
|
||||
"""
|
||||
# Summary
|
||||
|
||||
Utilities for building API documentation from an OpenAPI specification.
|
||||
"""
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
|
||||
import click
|
||||
|
||||
SWAGGER_SPEC_FILENAME = "openapi.json"
|
||||
|
||||
|
||||
@dataclass
|
||||
class OpenAPIMetadata:
|
||||
"""
|
||||
Metadata derived from the ``info`` block of an OpenAPI specification.
|
||||
|
||||
Attributes:
|
||||
site_name: Spec title, used as the MkDocs site name.
|
||||
site_description: Spec description, used as the site description.
|
||||
site_author: Contact name (fallback: contact email), used as the
|
||||
site author.
|
||||
"""
|
||||
|
||||
site_name: str
|
||||
site_description: str | None
|
||||
site_author: str | None
|
||||
|
||||
|
||||
def load_openapi_spec(spec_path: Path) -> dict:
|
||||
"""
|
||||
Load and validate an OpenAPI specification from a JSON file.
|
||||
|
||||
Args:
|
||||
spec_path: Path to the OpenAPI JSON specification file.
|
||||
|
||||
Returns:
|
||||
dict:
|
||||
The parsed OpenAPI specification.
|
||||
|
||||
Raises:
|
||||
click.ClickException:
|
||||
If the file cannot be read or the ``info`` block is invalid.
|
||||
"""
|
||||
if not spec_path.exists():
|
||||
raise click.ClickException(f"OpenAPI spec not found: {spec_path}")
|
||||
|
||||
try:
|
||||
data = json.loads(spec_path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError) as exc:
|
||||
raise click.ClickException(f"Unable to read OpenAPI spec: {exc}") from exc
|
||||
|
||||
if not isinstance(data, dict):
|
||||
raise click.ClickException("OpenAPI spec must be a JSON mapping")
|
||||
|
||||
info = data.get("info")
|
||||
if not isinstance(info, dict) or not isinstance(info.get("title"), str):
|
||||
raise click.ClickException("OpenAPI spec missing 'info.title'")
|
||||
|
||||
return data
|
||||
|
||||
|
||||
def derive_metadata(spec: dict) -> OpenAPIMetadata:
|
||||
"""
|
||||
Derive MkDocs site metadata from an OpenAPI spec ``info`` block.
|
||||
|
||||
Args:
|
||||
spec: Parsed OpenAPI specification.
|
||||
|
||||
Returns:
|
||||
OpenAPIMetadata:
|
||||
Site name, description, and author derived from the spec.
|
||||
"""
|
||||
info = spec["info"]
|
||||
|
||||
contact = info.get("contact")
|
||||
if isinstance(contact, dict):
|
||||
author = contact.get("name") or contact.get("email")
|
||||
else:
|
||||
author = None
|
||||
|
||||
return OpenAPIMetadata(
|
||||
site_name=info.get("title", ""),
|
||||
site_description=info.get("description"),
|
||||
site_author=author,
|
||||
)
|
||||
|
||||
|
||||
def generate_api_sources(spec: dict, docs_dir: Path) -> None:
|
||||
"""
|
||||
Generate swagger-enabled Markdown sources and the spec copy.
|
||||
|
||||
The specification is written as ``openapi.json`` inside ``docs_dir`` and
|
||||
an ``index.md`` embedding the swagger UI is generated alongside it.
|
||||
|
||||
Args:
|
||||
spec: Parsed OpenAPI specification.
|
||||
docs_dir: Directory (for example ``docs/api``) where the swagger
|
||||
sources are written.
|
||||
"""
|
||||
docs_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
spec_path = docs_dir / SWAGGER_SPEC_FILENAME
|
||||
spec_json = json.dumps(spec, indent=2)
|
||||
if not spec_path.exists() or spec_path.read_text(encoding="utf-8") != spec_json:
|
||||
spec_path.write_text(spec_json, encoding="utf-8")
|
||||
|
||||
index_path = docs_dir / "index.md"
|
||||
content = "# API Reference\n\n" f'<swagger-ui src="{SWAGGER_SPEC_FILENAME}"/>\n'
|
||||
if not index_path.exists() or index_path.read_text(encoding="utf-8") != content:
|
||||
index_path.write_text(content, encoding="utf-8")
|
||||
Reference in New Issue
Block a user