diff --git a/src/apiup/cli.py b/src/apiup/cli.py
index 246b9cc..a5186b7 100644
--- a/src/apiup/cli.py
+++ b/src/apiup/cli.py
@@ -75,6 +75,8 @@ def main() -> None:
print(f" Spec : {cfg.spec}")
print(f" Mode : {cfg.mode}")
print(f" Listen: http://{cfg.host}:{cfg.port}")
+ print(f" Docs : http://{cfg.host}:{cfg.port}/docs")
+ print(f" Spec : http://{cfg.host}:{cfg.port}/spec.json")
print(f" Routes: {len(routes)}")
print()
for r in routes:
@@ -84,7 +86,7 @@ def main() -> None:
# ── Build + serve ────────────────────────────────────────────────────────
from apiup.server import build_mock_app, serve
- app = build_mock_app(routes, spec)
+ app = build_mock_app(routes, spec, cfg.spec)
serve(app, host=cfg.host, port=cfg.port)
diff --git a/src/apiup/server.py b/src/apiup/server.py
index aac0d0b..e63c624 100644
--- a/src/apiup/server.py
+++ b/src/apiup/server.py
@@ -7,36 +7,56 @@
import json
import re
import sys
+from pathlib import Path
from typing import Any
from apiup.mock import extract_mock_response
from apiup.spec import Route
try:
- from litestar import Litestar
+ from litestar import Litestar, get
from litestar.handlers import HTTPRouteHandler
from litestar.response import Response as LitestarResponse
except ImportError: # pragma: no cover
Litestar = None # type: ignore[assignment,misc]
HTTPRouteHandler = None # type: ignore[assignment,misc]
LitestarResponse = None # type: ignore[assignment]
+ get = None # type: ignore[assignment]
-# OpenAPI {param} → Litestar {param:str}
_PARAM_RE = re.compile(r"\{(\w+)\}")
-
-# Status codes that must not have a response body
_NO_BODY_CODES = frozenset({204, 304})
+_SWAGGER_HTML = """
+
+
+ apiup — Swagger UI
+
+
+
+
+
+
+
+
+
+"""
+
def _openapi_path_to_litestar(path: str) -> str:
- """Convert /skills/{skillId} → /skills/{skillId:str}."""
return _PARAM_RE.sub(r"{\1:str}", path)
def _make_handler(body: Any, status_code: int) -> Any:
- """Return a handler closed over body+status — no mutable defaults."""
if status_code in _NO_BODY_CODES:
- # No-body response — return None, Litestar sends empty 204/304
+
async def _handler() -> None:
return None
else:
@@ -52,8 +72,8 @@ async def _handler() -> Any: # type: ignore[misc]
return _handler
-def build_mock_app(routes: list[Route], spec: dict[str, Any]) -> Any:
- """Return a Litestar ASGI app with one handler per spec route."""
+def build_mock_app(routes: list[Route], spec: dict[str, Any], spec_path: str) -> Any:
+ """Return a Litestar ASGI app with mock handlers + swagger UI."""
if Litestar is None:
print(
"ERROR: litestar not installed.\nRun: pip install 'litestar[standard]'",
@@ -61,7 +81,27 @@ def build_mock_app(routes: list[Route], spec: dict[str, Any]) -> Any:
)
sys.exit(2)
- handlers = []
+ # ── built-in routes ────────────────────────────────────────────────────
+ spec_bytes: bytes = Path(spec_path).read_bytes()
+
+ @get("/spec.json", media_type="application/json")
+ async def serve_spec() -> LitestarResponse: # type: ignore[return]
+ return LitestarResponse(
+ content=spec_bytes,
+ status_code=200,
+ media_type="application/json",
+ )
+
+ @get("/docs", media_type="text/html")
+ async def serve_docs() -> LitestarResponse: # type: ignore[return]
+ return LitestarResponse(
+ content=_SWAGGER_HTML.encode(),
+ status_code=200,
+ media_type="text/html",
+ )
+
+ # ── mock route handlers ────────────────────────────────────────────────
+ handlers: list[Any] = [serve_spec, serve_docs]
for route in routes:
status_code, body = extract_mock_response(route.responses, spec)
@@ -79,7 +119,6 @@ def build_mock_app(routes: list[Route], spec: dict[str, Any]) -> Any:
def serve(app: Any, host: str, port: int) -> None:
- """Start uvicorn with the given app."""
try:
import uvicorn
except ImportError: