Skip to content

feat(openapi): first-class docs UI and OpenAPI 3 enrichment for Scalar/Swagger #130

Description

@ZhuchkaTriplesix

Problem or use case

Consumers building real APIs on OxyRoute (e.g. QueryaHub/MarketApi) need a usable interactive API explorer out of the box — the same DX FastAPI provides with /docs + /redoc.

Today OxyRoute only exposes a minimal GET /openapi.json (see docs/openapi.md). That document is enough for discovery, but not enough for Scalar / Swagger UI without app-side glue:

Gap Impact
No built-in docs UI (/docs, /api/docs, Scalar, Swagger UI) Every app reimplements HTML + CDN + CSP
Paths use matchit :param instead of OpenAPI {param} Scalar/Swagger do not bind path params correctly
No parameters entries for path params Try-it-out is incomplete
No components.securitySchemes / per-op security from require_jwt Authorize / Bearer JWT missing in UI
No tags / servers / richer info hooks Flat, hard-to-navigate spec
Docs show async def __rsgi_init__, but Granian RSGI calls sync __rsgi_init__(loop) with a non-running loop Lifespan silently skipped (coroutine was never awaited); pools never open

MarketApi currently works around this with an enriched /openapi.json, Scalar at /api/docs, Swagger at /api/swagger, and a sync Granian-compatible lifespan — that glue belongs in the framework.

Proposed solution

1. Docs UI (P0)

Ship an official optional UI (prefer Scalar, also fine to offer Swagger UI):

  • App(..., include_openapi=True, docs_ui="scalar" | "swagger" | None)
  • or helpers: app.mount_docs("/docs", ui="scalar") / include_docs_ui=True
  • Default route: GET /docs (and optionally /redoc / /swagger)
  • Spec URL: existing /openapi.json
  • Document CSP notes when loading UI from CDN, or vendor a small static bundle via StaticFiles

2. OpenAPI enrichment (P0)

When building / serving the document:

  1. Convert path templates :name{name} and emit OpenAPI parameters (in: path, required: true).
  2. If a route was registered with require_jwt=True, attach:
    • components.securitySchemes.bearerAuth (http + bearer + JWT)
    • security: [{ bearerAuth: [] }] on that operation (public routes keep security: [] or omit global security).
  3. Support tags from include_router(..., tags=[...]) or an explicit openapi_tags= / route tags= kwarg.
  4. Allow setting servers, info.description, info.contact from Python (set_openapi_info(...) / constructor kwargs).

Keep body_schema / body_model as today; document them as the path to rich request bodies in the UI.

3. Lifespan contract (P0 — docs + helper)

Align docs and base App with the Granian RSGI spec:

def __rsgi_init__(self, loop):
    loop.run_until_complete(self._async_startup())
  • Update docs/rsgi.md and examples (examples/rsgi_lifespan_app.py) — async override alone is wrong under Granian.
  • Optionally provide a base helper that apps can override as async def on_startup(self) / on_shutdown, while the framework’s sync __rsgi_init__ / __rsgi_del__ call loop.run_until_complete.
  • Keep TestClient behavior: no-arg call may still return/await a coroutine.

4. Nice-to-haves (P1)

  • Response content schemas / status codes beyond bare 200
  • Multipart upload documented for read_form_body routes
  • Export enriched spec via openapi_json() (same document the UI uses)
  • CHANGELOG entry for the OpenAPI/docs surface in the next minor

Acceptance criteria

  • With docs_ui (or equivalent) enabled, opening /docs in a browser shows Scalar or Swagger against /openapi.json without app-specific HTML.
  • /openapi.json uses {param} paths and lists path parameters.
  • JWT-protected routes show Authorize / Bearer in the UI.
  • Tags appear when routers declare them.
  • Docs + example lifespan work under granian --interface rsgi (DB/redis init actually runs; no “never awaited” warning).
  • Tests cover path conversion, security scheme emission, and docs route 200 + text/html.
  • docs/openapi.md updated.

Context

Priority

P0 for DX — without this, every OxyRoute API reimplements FastAPI-like docs and hits the Granian lifespan footgun.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions